Skip to content

Commit

Permalink
use custom JSON encoding if specified (Netflix#1477)
Browse files Browse the repository at this point in the history
If a class extended JsonSupport and provided a custom
encoding it would get used by the interited methods, but
would get ignored when using `Json.encode`. This could
create problems when an object is nested because the
custom encode would not get used. The `Json.encode`
could not directly call encode, because it would create
a loop with the default implementation.

This change adds a method that can indicate if it is
desirable for `Json.ecnode` to use the custom encoding.
  • Loading branch information
brharrington authored and manolama committed May 22, 2024
1 parent 2d4f7d7 commit 85ffde4
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ final case class ArrayData(values: Array[Double]) extends ChunkData {

def typeName: String = "array"

override def hasCustomEncoding: Boolean = true

override def encode(gen: JsonGenerator): Unit = {
gen.writeStartObject()
gen.writeStringField("type", "array")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ case class LwcDatapoint(timestamp: Long, id: String, tags: Map[String, String],

val `type`: String = "datapoint"

override def hasCustomEncoding: Boolean = true

override def encode(gen: JsonGenerator): Unit = {
gen.writeStartObject()
gen.writeStringField("type", `type`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ case class TimeSeriesMessage(
data: ChunkData
) extends JsonSupport {

override def hasCustomEncoding: Boolean = true

override def encode(gen: JsonGenerator): Unit = {
gen.writeStartObject()
gen.writeStringField("type", "timeseries")
Expand Down
4 changes: 4 additions & 0 deletions atlas-json/src/main/scala/com/netflix/atlas/json/Json.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.fasterxml.jackson.core._
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.core.json.JsonWriteFeature
import com.fasterxml.jackson.databind._
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.dataformat.smile.SmileFactory
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
Expand Down Expand Up @@ -87,6 +88,9 @@ object Json {
mapper.registerModule(DefaultScalaModule)
mapper.registerModule(new JavaTimeModule)
mapper.registerModule(new Jdk8Module)
mapper.registerModule(
new SimpleModule().setSerializerModifier(new JsonSupportSerializerModifier)
)
mapper
}

Expand Down
15 changes: 15 additions & 0 deletions atlas-json/src/main/scala/com/netflix/atlas/json/JsonSupport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,27 @@ import com.fasterxml.jackson.core.JsonGenerator

import scala.util.Using

/**
* Trait that adds methods to easily encode the object to JSON. Can be used to indicate
* a type that has a known mapping to JSON format.
*/
trait JsonSupport {

/** Returns true if a custom encoding is used that does not rely on `Json.encode`. */
def hasCustomEncoding: Boolean = false

/**
* Encode this object as JSON. By default it will just use `Json.encode`. This method
* can be overridden to customize the format or to provide a more performance implementation.
* When using a custom format, the subclass should also override `hasCustomEncoding` to
* return true. This will cause `Json.encode` to use the custom implementation rather than
* the default serializer for the type.
*/
def encode(gen: JsonGenerator): Unit = {
Json.encode(gen, this)
}

/** Returns a JSON string representing this object. */
final def toJson: String = {
Using.resource(new StringWriter) { w =>
Using.resource(Json.newJsonGenerator(w)) { gen =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2014-2022 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.atlas.json

import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider

private[json] class JsonSupportSerializer(
defaultSerializer: JsonSerializer[AnyRef]
) extends JsonSerializer[JsonSupport] {

override def serialize(
value: JsonSupport,
gen: JsonGenerator,
serializers: SerializerProvider
): Unit = {

if (value.hasCustomEncoding)
value.encode(gen)
else
defaultSerializer.serialize(value, gen, serializers)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2014-2022 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.atlas.json

import com.fasterxml.jackson.databind.BeanDescription
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializationConfig
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier

private[json] class JsonSupportSerializerModifier extends BeanSerializerModifier {

override def modifySerializer(
config: SerializationConfig,
beanDesc: BeanDescription,
serializer: JsonSerializer[_]
): JsonSerializer[_] = {

if (classOf[JsonSupport].isAssignableFrom(beanDesc.getBeanClass))
new JsonSupportSerializer(serializer.asInstanceOf[JsonSerializer[AnyRef]])
else
serializer
}
}
18 changes: 16 additions & 2 deletions atlas-json/src/test/scala/com/netflix/atlas/json/JsonSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -460,9 +460,19 @@ class JsonSuite extends FunSuite {
}

test("JsonSupport NaN encoding") {
val obj = JsonObjectWithSupport(Double.NaN)
val obj = JsonObjectWithDefaultSupport(Double.NaN)
assertEquals(obj.toJson, """{"v":"NaN"}""")
}

test("JsonSupport default encoding") {
val obj = JsonObjectWithDefaultSupport(42.0)
assertEquals(Json.encode(List(obj)), """[{"v":42.0}]""")
}

test("JsonSupport custom encoding") {
val obj = JsonObjectWithSupport(42.0)
assertEquals(Json.encode(List(obj)), """[{"custom":42.0}]""")
}
}

case class JsonKeyWithDot(`a.b`: String)
Expand Down Expand Up @@ -499,11 +509,15 @@ case class JsonSuiteObjectWithDefaults(
values: List[String] = Nil
)

case class JsonObjectWithDefaultSupport(v: Double) extends JsonSupport

case class JsonObjectWithSupport(v: Double) extends JsonSupport {

override def hasCustomEncoding: Boolean = true

override def encode(gen: JsonGenerator): Unit = {
gen.writeStartObject()
gen.writeNumberField("v", v)
gen.writeNumberField("custom", v)
gen.writeEndObject()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ case class StreamMetadata(
droppedMessages.getCurrent.addAndGet(n)
}

override def hasCustomEncoding: Boolean = true

override def encode(gen: JsonGenerator): Unit = {
gen.writeStartObject()
gen.writeStringField("streamId", streamId)
Expand Down

0 comments on commit 85ffde4

Please sign in to comment.