diff --git a/client-runtime/src/main/java/com/microsoft/rest/serializer/FlatteningDeserializer.java b/client-runtime/src/main/java/com/microsoft/rest/serializer/FlatteningDeserializer.java index 07054eb2d9..c9054c7d99 100644 --- a/client-runtime/src/main/java/com/microsoft/rest/serializer/FlatteningDeserializer.java +++ b/client-runtime/src/main/java/com/microsoft/rest/serializer/FlatteningDeserializer.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.deser.BeanDeserializer; import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; @@ -35,6 +34,8 @@ * will be mapped to a top level "name" property in the POJO model. */ public final class FlatteningDeserializer extends StdDeserializer implements ResolvableDeserializer { + private static final long serialVersionUID = -2133095337545715498L; + /** * The default mapperAdapter for the current type. */ @@ -68,9 +69,12 @@ public static SimpleModule getModule(final ObjectMapper mapper) { SimpleModule module = new SimpleModule(); module.setDeserializerModifier(new BeanDeserializerModifier() { @Override - public JsonDeserializer modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer deserializer) { - if (BeanDeserializer.class.isAssignableFrom(deserializer.getClass())) { - // Apply flattening deserializer on all POJO types. + public JsonDeserializer modifyDeserializer(DeserializationConfig config, + BeanDescription beanDesc, + JsonDeserializer deserializer) { + if (beanDesc.getBeanClass().getAnnotation(JsonFlatten.class) != null) { + // Register 'FlatteningDeserializer' for complex type so that 'deserializeWithType' + // will get called for complex types and it can analyze typeId discriminator. return new FlatteningDeserializer(beanDesc.getBeanClass(), deserializer, mapper); } else { return deserializer; @@ -82,27 +86,26 @@ public JsonDeserializer modifyDeserializer(DeserializationConfig config, Bean @SuppressWarnings("unchecked") @Override - public Object deserializeWithType(JsonParser jp, DeserializationContext cxt, TypeDeserializer tDeserializer) throws IOException { - // This method will be called by Jackson for each "Json object with TypeId" in the input wire stream - // it is trying to deserialize. - // The below variable 'currentJsonNode' will hold the JsonNode corresponds to current - // Json object this method is called to handle. + public Object deserializeWithType(JsonParser jp, + DeserializationContext cxt, + TypeDeserializer tDeserializer) throws IOException { + // This method will be called from Jackson for each "Json object with TypeId" as it + // process the input data. This enable us to pre-process then give it to the next + // deserializer in the Jackson pipeline. + // + // The parameter 'jp' is the reader to read "Json object with TypeId" // JsonNode currentJsonNode = mapper.readTree(jp); final Class tClass = this.defaultDeserializer.handledType(); for (Class c : TypeToken.of(tClass).getTypes().classes().rawTypes()) { - if (c.isAssignableFrom(Object.class)) { - continue; - } else { - final JsonTypeInfo typeInfo = c.getAnnotation(com.fasterxml.jackson.annotation.JsonTypeInfo.class); - if (typeInfo != null) { - String typeId = typeInfo.property(); - if (containsDot(typeId)) { - final String typeIdOnWire = unescapeEscapedDots(typeId); - JsonNode typeIdValue = ((ObjectNode) currentJsonNode).remove(typeIdOnWire); - if (typeIdValue != null) { - ((ObjectNode) currentJsonNode).put(typeId, typeIdValue); - } + final JsonTypeInfo typeInfo = c.getAnnotation(com.fasterxml.jackson.annotation.JsonTypeInfo.class); + if (typeInfo != null) { + String typeId = typeInfo.property(); + if (containsDot(typeId)) { + final String typeIdOnWire = unescapeEscapedDots(typeId); + JsonNode typeIdValue = ((ObjectNode) currentJsonNode).remove(typeIdOnWire); + if (typeIdValue != null) { + ((ObjectNode) currentJsonNode).put(typeId, typeIdValue); } } } @@ -114,8 +117,8 @@ public Object deserializeWithType(JsonParser jp, DeserializationContext cxt, Typ public Object deserialize(JsonParser jp, DeserializationContext cxt) throws IOException { // This method will be called by Jackson for each "Json object" in the input wire stream // it is trying to deserialize. - // The below variable 'currentJsonNode' will hold the JsonNode corresponds to current - // Json object this method is called to handle. + // + // The parameter 'jp' is the reader to read "Json object with TypeId" // JsonNode currentJsonNode = mapper.readTree(jp); if (currentJsonNode.isNull()) { @@ -152,15 +155,34 @@ private static void handleFlatteningForField(Field classField, JsonNode jsonNode final JsonProperty jsonProperty = classField.getAnnotation(JsonProperty.class); if (jsonProperty != null) { final String jsonPropValue = jsonProperty.value(); + if (jsonNode.has(jsonPropValue)) { + // There is an additional property with it's key conflicting with the + // JsonProperty value, escape this additional property's key. + final String escapedJsonPropValue = jsonPropValue.replace(".", "\\."); + ((ObjectNode) jsonNode).set(escapedJsonPropValue, jsonNode.get(jsonPropValue)); + } if (containsFlatteningDots(jsonPropValue)) { + // The jsonProperty value contains flattening dots, uplift the nested + // json node that this value resolving to the current level. JsonNode childJsonNode = findNestedNode(jsonNode, jsonPropValue); - ((ObjectNode) jsonNode).put(jsonPropValue, childJsonNode); + ((ObjectNode) jsonNode).set(jsonPropValue, childJsonNode); } } } /** - * Given a json node, find a nested node using given composed key. + * Checks whether the given key has flattening dots in it. + * Flattening dots are dot '.' characters those are not preceded by slash '\' + * + * @param key the key + * @return true if the key has flattening dots, false otherwise. + */ + private static boolean containsFlatteningDots(String key) { + return key.matches(".+[^\\\\]\\..+"); + } + + /** + * Given a json node, find a nested node in it identified by the given composed key. * * @param jsonNode the parent json node * @param composedKey a key combines multiple keys using flattening dots. @@ -179,17 +201,6 @@ private static JsonNode findNestedNode(JsonNode jsonNode, String composedKey) { return jsonNode; } - /** - * Checks whether the given key has flattening dots in it. - * Flattening dots are dot character '.' those are not preceded by slash '\' - * - * @param key the key - * @return true if the key has flattening dots, false otherwise. - */ - private static boolean containsFlatteningDots(String key) { - return key.matches(".+[^\\\\]\\..+"); - } - /** * Split the key by flattening dots. * Flattening dots are dot character '.' those are not preceded by slash '\' @@ -220,18 +231,19 @@ private static String unescapeEscapedDots(String key) { * @return true if at least one dot found */ private static boolean containsDot(String str) { - return str != null && str != "" && str.contains("."); + return str != null && !str.isEmpty() && str.contains("."); } /** * Create a JsonParser for a given json node. + * * @param jsonNode the json node * @return the json parser - * @throws IOException + * @throws IOException if underlying reader fails to read the json string */ private static JsonParser newJsonParserForNode(JsonNode jsonNode) throws IOException { JsonParser parser = new JsonFactory().createParser(jsonNode.toString()); parser.nextToken(); return parser; } -} +} \ No newline at end of file diff --git a/client-runtime/src/test/java/com/microsoft/rest/AdditionalPropertiesSerializerTests.java b/client-runtime/src/test/java/com/microsoft/rest/AdditionalPropertiesSerializerTests.java index 3a5cec44fb..c80de5b80b 100644 --- a/client-runtime/src/test/java/com/microsoft/rest/AdditionalPropertiesSerializerTests.java +++ b/client-runtime/src/test/java/com/microsoft/rest/AdditionalPropertiesSerializerTests.java @@ -34,7 +34,8 @@ public void canSerializeAdditionalProperties() throws Exception { foo.additionalProperties.put("properties.bar", "barbar"); String serialized = new JacksonAdapter().serialize(foo); - Assert.assertEquals("{\"$type\":\"foo\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}", serialized); + String expected = "{\"$type\":\"foo\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}"; + Assert.assertEquals(expected, serialized); } @Test diff --git a/client-runtime/src/test/java/com/microsoft/rest/AnimalWithTypeIdContainingDot.java b/client-runtime/src/test/java/com/microsoft/rest/AnimalWithTypeIdContainingDot.java index 640914f712..7ed3ea0dd8 100644 --- a/client-runtime/src/test/java/com/microsoft/rest/AnimalWithTypeIdContainingDot.java +++ b/client-runtime/src/test/java/com/microsoft/rest/AnimalWithTypeIdContainingDot.java @@ -3,7 +3,9 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; +import com.microsoft.rest.serializer.JsonFlatten; +@JsonFlatten @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@odata\\.type", defaultImpl = AnimalWithTypeIdContainingDot.class) @JsonTypeName("AnimalWithTypeIdContainingDot") @JsonSubTypes({ diff --git a/client-runtime/src/test/java/com/microsoft/rest/CatWithTypeIdContainingDot.java b/client-runtime/src/test/java/com/microsoft/rest/CatWithTypeIdContainingDot.java index 86b028cc33..38d013caa9 100644 --- a/client-runtime/src/test/java/com/microsoft/rest/CatWithTypeIdContainingDot.java +++ b/client-runtime/src/test/java/com/microsoft/rest/CatWithTypeIdContainingDot.java @@ -3,7 +3,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; +import com.microsoft.rest.serializer.JsonFlatten; +@JsonFlatten @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@odata\\.type", defaultImpl = CatWithTypeIdContainingDot.class) @JsonTypeName("#Favourite.Pet.CatWithTypeIdContainingDot") public class CatWithTypeIdContainingDot extends AnimalWithTypeIdContainingDot { diff --git a/client-runtime/src/test/java/com/microsoft/rest/DogWithTypeIdContainingDot.java b/client-runtime/src/test/java/com/microsoft/rest/DogWithTypeIdContainingDot.java index ecdecc5a01..ecbce26e18 100644 --- a/client-runtime/src/test/java/com/microsoft/rest/DogWithTypeIdContainingDot.java +++ b/client-runtime/src/test/java/com/microsoft/rest/DogWithTypeIdContainingDot.java @@ -3,7 +3,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; +import com.microsoft.rest.serializer.JsonFlatten; +@JsonFlatten @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@odata\\.type", defaultImpl = DogWithTypeIdContainingDot.class) @JsonTypeName("#Favourite.Pet.DogWithTypeIdContainingDot") public class DogWithTypeIdContainingDot extends AnimalWithTypeIdContainingDot { diff --git a/client-runtime/src/test/java/com/microsoft/rest/NonEmptyAnimalWithTypeIdContainingDot.java b/client-runtime/src/test/java/com/microsoft/rest/NonEmptyAnimalWithTypeIdContainingDot.java index ec13e5f650..97e972638a 100644 --- a/client-runtime/src/test/java/com/microsoft/rest/NonEmptyAnimalWithTypeIdContainingDot.java +++ b/client-runtime/src/test/java/com/microsoft/rest/NonEmptyAnimalWithTypeIdContainingDot.java @@ -4,7 +4,9 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; +import com.microsoft.rest.serializer.JsonFlatten; +@JsonFlatten @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@odata\\.type", defaultImpl = NonEmptyAnimalWithTypeIdContainingDot.class) @JsonTypeName("NonEmptyAnimalWithTypeIdContainingDot") @JsonSubTypes({ diff --git a/client-runtime/src/test/java/com/microsoft/rest/RabbitWithTypeIdContainingDot.java b/client-runtime/src/test/java/com/microsoft/rest/RabbitWithTypeIdContainingDot.java index e77f741e50..4cd1b6d588 100644 --- a/client-runtime/src/test/java/com/microsoft/rest/RabbitWithTypeIdContainingDot.java +++ b/client-runtime/src/test/java/com/microsoft/rest/RabbitWithTypeIdContainingDot.java @@ -3,9 +3,11 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; +import com.microsoft.rest.serializer.JsonFlatten; import java.util.List; +@JsonFlatten @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@odata\\.type", defaultImpl = RabbitWithTypeIdContainingDot.class) @JsonTypeName("#Favourite.Pet.RabbitWithTypeIdContainingDot") public class RabbitWithTypeIdContainingDot extends AnimalWithTypeIdContainingDot { diff --git a/client-runtime/src/test/java/com/microsoft/rest/TurtleWithTypeIdContainingDot.java b/client-runtime/src/test/java/com/microsoft/rest/TurtleWithTypeIdContainingDot.java index 1245e821de..79d18dda15 100644 --- a/client-runtime/src/test/java/com/microsoft/rest/TurtleWithTypeIdContainingDot.java +++ b/client-runtime/src/test/java/com/microsoft/rest/TurtleWithTypeIdContainingDot.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@odata\\.type", defaultImpl = TurtleWithTypeIdContainingDot.class) +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@odata.type", defaultImpl = TurtleWithTypeIdContainingDot.class) @JsonTypeName("#Favourite.Pet.TurtleWithTypeIdContainingDot") public class TurtleWithTypeIdContainingDot extends NonEmptyAnimalWithTypeIdContainingDot { @JsonProperty(value = "size")