From 85ffde439ff678af4646663339445f83e31079c7 Mon Sep 17 00:00:00 2001 From: brharrington Date: Fri, 28 Oct 2022 17:44:34 -0500 Subject: [PATCH] use custom JSON encoding if specified (#1477) 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. --- .../netflix/atlas/eval/model/ChunkData.scala | 2 + .../atlas/eval/model/LwcDatapoint.scala | 2 + .../atlas/eval/model/TimeSeriesMessage.scala | 2 + .../scala/com/netflix/atlas/json/Json.scala | 4 ++ .../com/netflix/atlas/json/JsonSupport.scala | 15 ++++++++ .../atlas/json/JsonSupportSerializer.scala | 37 +++++++++++++++++++ .../json/JsonSupportSerializerModifier.scala | 36 ++++++++++++++++++ .../com/netflix/atlas/json/JsonSuite.scala | 18 ++++++++- .../netflix/atlas/lwcapi/StreamMetadata.scala | 2 + 9 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 atlas-json/src/main/scala/com/netflix/atlas/json/JsonSupportSerializer.scala create mode 100644 atlas-json/src/main/scala/com/netflix/atlas/json/JsonSupportSerializerModifier.scala diff --git a/atlas-eval/src/main/scala/com/netflix/atlas/eval/model/ChunkData.scala b/atlas-eval/src/main/scala/com/netflix/atlas/eval/model/ChunkData.scala index 448a3cf45..ad8c7ed4b 100644 --- a/atlas-eval/src/main/scala/com/netflix/atlas/eval/model/ChunkData.scala +++ b/atlas-eval/src/main/scala/com/netflix/atlas/eval/model/ChunkData.scala @@ -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") diff --git a/atlas-eval/src/main/scala/com/netflix/atlas/eval/model/LwcDatapoint.scala b/atlas-eval/src/main/scala/com/netflix/atlas/eval/model/LwcDatapoint.scala index d9aa2c434..f1ef82b8a 100644 --- a/atlas-eval/src/main/scala/com/netflix/atlas/eval/model/LwcDatapoint.scala +++ b/atlas-eval/src/main/scala/com/netflix/atlas/eval/model/LwcDatapoint.scala @@ -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`) diff --git a/atlas-eval/src/main/scala/com/netflix/atlas/eval/model/TimeSeriesMessage.scala b/atlas-eval/src/main/scala/com/netflix/atlas/eval/model/TimeSeriesMessage.scala index 25da5e97b..d82a5bc0b 100644 --- a/atlas-eval/src/main/scala/com/netflix/atlas/eval/model/TimeSeriesMessage.scala +++ b/atlas-eval/src/main/scala/com/netflix/atlas/eval/model/TimeSeriesMessage.scala @@ -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") diff --git a/atlas-json/src/main/scala/com/netflix/atlas/json/Json.scala b/atlas-json/src/main/scala/com/netflix/atlas/json/Json.scala index c16d8e644..f4d09e138 100644 --- a/atlas-json/src/main/scala/com/netflix/atlas/json/Json.scala +++ b/atlas-json/src/main/scala/com/netflix/atlas/json/Json.scala @@ -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 @@ -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 } diff --git a/atlas-json/src/main/scala/com/netflix/atlas/json/JsonSupport.scala b/atlas-json/src/main/scala/com/netflix/atlas/json/JsonSupport.scala index 83a866d85..1c3653e0b 100644 --- a/atlas-json/src/main/scala/com/netflix/atlas/json/JsonSupport.scala +++ b/atlas-json/src/main/scala/com/netflix/atlas/json/JsonSupport.scala @@ -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 => diff --git a/atlas-json/src/main/scala/com/netflix/atlas/json/JsonSupportSerializer.scala b/atlas-json/src/main/scala/com/netflix/atlas/json/JsonSupportSerializer.scala new file mode 100644 index 000000000..8287baceb --- /dev/null +++ b/atlas-json/src/main/scala/com/netflix/atlas/json/JsonSupportSerializer.scala @@ -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) + } +} diff --git a/atlas-json/src/main/scala/com/netflix/atlas/json/JsonSupportSerializerModifier.scala b/atlas-json/src/main/scala/com/netflix/atlas/json/JsonSupportSerializerModifier.scala new file mode 100644 index 000000000..25956fe52 --- /dev/null +++ b/atlas-json/src/main/scala/com/netflix/atlas/json/JsonSupportSerializerModifier.scala @@ -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 + } +} diff --git a/atlas-json/src/test/scala/com/netflix/atlas/json/JsonSuite.scala b/atlas-json/src/test/scala/com/netflix/atlas/json/JsonSuite.scala index 1c8082173..f8fa01537 100644 --- a/atlas-json/src/test/scala/com/netflix/atlas/json/JsonSuite.scala +++ b/atlas-json/src/test/scala/com/netflix/atlas/json/JsonSuite.scala @@ -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) @@ -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() } } diff --git a/atlas-lwcapi/src/main/scala/com/netflix/atlas/lwcapi/StreamMetadata.scala b/atlas-lwcapi/src/main/scala/com/netflix/atlas/lwcapi/StreamMetadata.scala index 32c5c2800..28050de37 100644 --- a/atlas-lwcapi/src/main/scala/com/netflix/atlas/lwcapi/StreamMetadata.scala +++ b/atlas-lwcapi/src/main/scala/com/netflix/atlas/lwcapi/StreamMetadata.scala @@ -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)