diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 3ba81c10dec..73f5c66d1a0 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -1583,16 +1583,18 @@ Hervé Boutemy (hboutemy@github) (2.15.0) Sim Yih Tsern (yihtsern@github) - * Contributed fix for #2974: Null coercion with `@JsonSetter` does not work with `java.lang.Record` + * Contributed fix for #2974: Null coercion with `@JsonSetter` does not work with `java.lang.Record` (2.15.0) - * Contributed fix for #2992: Properties naming strategy do not work with Record + * Contributed fix for #2992: Properties naming strategy do not work with Record (2.15.0) - * Contributed fix for #3180: Support `@JsonCreator` annotation on record classes + * Contributed fix for #3180: Support `@JsonCreator` annotation on record classes (2.15.0) - * Contributed fix for #3297: `@JsonDeserialize(converter = ...)` does not work with Records + * Contributed fix for #3297: `@JsonDeserialize(converter = ...)` does not work with Records (2.15.0) - * Contributed fix for #3342: `JsonTypeInfo.As.EXTERNAL_PROPERTY` does not work with record wrappers + * Contributed fix for #3342: `JsonTypeInfo.As.EXTERNAL_PROPERTY` does not work with record wrappers (2.15.0) + * Contributed fix for #3894: Only avoid Records fields detection for deserialization + (2.15.1) Ajay Siwach (Siwach16@github) * Contributed #3637: Add enum features into `@JsonFormat.Feature` diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 066ac3af29d..98557543936 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -8,6 +8,15 @@ Project: jackson-databind No changes since 2.15 +2.15.1 (not yet released) + +#3894: Only avoid Records fields detection for deserialization + (contributed by Sim Y-T) +#3913: Issue with deserialization when there are unexpected properties (due + to null `StreamReadConstraints`) + (reported by @sbertault) +#3914: Fix TypeId serialization for `JsonTypeInfo.Id.DEDUCTION`, native type ids + 2.15.0 (23-Apr-2023) #2536: Add `EnumFeature.READ_ENUM_KEYS_USING_INDEX` to work with diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java index 6a16a75d516..f2487ff713d 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java @@ -454,7 +454,7 @@ protected Object _deserializeUsingPropertyBased(final JsonParser p, final Deseri // polymorphic? if (bean.getClass() != _beanType.getRawClass()) { - return handlePolymorphic(p, ctxt, bean, unknown); + return handlePolymorphic(p, ctxt, p.streamReadConstraints(), bean, unknown); } if (unknown != null) { // nope, just extra unknown stuff... bean = handleUnknownProperties(ctxt, bean, unknown); @@ -538,7 +538,7 @@ protected Object _deserializeUsingPropertyBased(final JsonParser p, final Deseri if (unknown != null) { // polymorphic? if (bean.getClass() != _beanType.getRawClass()) { // lgtm [java/dereferenced-value-may-be-null] - return handlePolymorphic(null, ctxt, bean, unknown); + return handlePolymorphic(null, ctxt, p.streamReadConstraints(), bean, unknown); } // no, just some extra unknown properties return handleUnknownProperties(ctxt, bean, unknown); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java index 7f1b859f40e..cb36d2dc897 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java @@ -1735,10 +1735,33 @@ protected void handleIgnoredProperty(JsonParser p, DeserializationContext ctxt, * @param p (optional) If not null, parser that has more properties to handle * (in addition to buffered properties); if null, all properties are passed * in buffer + * @deprecated use {@link #handlePolymorphic(JsonParser, DeserializationContext, StreamReadConstraints, Object, TokenBuffer)} */ + @Deprecated protected Object handlePolymorphic(JsonParser p, DeserializationContext ctxt, Object bean, TokenBuffer unknownTokens) throws IOException + { + final StreamReadConstraints streamReadConstraints = p == null ? + StreamReadConstraints.defaults() : p.streamReadConstraints(); + return handlePolymorphic(p, ctxt, streamReadConstraints, bean, unknownTokens); + } + + /** + * Method called in cases where we may have polymorphic deserialization + * case: that is, type of Creator-constructed bean is not the type + * of deserializer itself. It should be a sub-class or implementation + * class; either way, we may have more specific deserializer to use + * for handling it. + * + * @param p (optional) If not null, parser that has more properties to handle + * (in addition to buffered properties); if null, all properties are passed + * in buffer + * @since 2.15.1 + */ + protected Object handlePolymorphic(JsonParser p, DeserializationContext ctxt, + StreamReadConstraints streamReadConstraints, Object bean, TokenBuffer unknownTokens) + throws IOException { // First things first: maybe there is a more specific deserializer available? JsonDeserializer subDeser = _findSubclassDeserializer(ctxt, bean, unknownTokens); @@ -1746,7 +1769,7 @@ protected Object handlePolymorphic(JsonParser p, DeserializationContext ctxt, if (unknownTokens != null) { // need to add END_OBJECT marker first unknownTokens.writeEndObject(); - JsonParser p2 = unknownTokens.asParser(p.streamReadConstraints()); + JsonParser p2 = unknownTokens.asParser(streamReadConstraints); p2.nextToken(); // to get to first data field bean = subDeser.deserialize(p2, ctxt, bean); } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BuilderBasedDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/BuilderBasedDeserializer.java index 50e2dce1693..628e01da27c 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BuilderBasedDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BuilderBasedDeserializer.java @@ -395,7 +395,7 @@ protected Object _deserializeUsingPropertyBased(final JsonParser p, } // polymorphic? if (builder.getClass() != _beanType.getRawClass()) { - return handlePolymorphic(p, ctxt, builder, unknown); + return handlePolymorphic(p, ctxt, p.streamReadConstraints(), builder, unknown); } if (unknown != null) { // nope, just extra unknown stuff... builder = handleUnknownProperties(ctxt, builder, unknown); @@ -440,7 +440,7 @@ protected Object _deserializeUsingPropertyBased(final JsonParser p, if (unknown != null) { // polymorphic? if (builder.getClass() != _beanType.getRawClass()) { - return handlePolymorphic(null, ctxt, builder, unknown); + return handlePolymorphic(null, ctxt, p.streamReadConstraints(), builder, unknown); } // no, just some extra unknown properties return handleUnknownProperties(ctxt, builder, unknown); @@ -669,7 +669,7 @@ protected Object deserializeUsingPropertyBasedWithUnwrapped(JsonParser p, continue; // never gets here } if (builder.getClass() != _beanType.getRawClass()) { - return handlePolymorphic(p, ctxt, builder, tokens); + return handlePolymorphic(p, ctxt, p.streamReadConstraints(), builder, tokens); } return deserializeWithUnwrapped(p, ctxt, builder, tokens); } diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java index fcffc61e1b2..fabeb15cbb2 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java @@ -440,7 +440,8 @@ protected void collectAll() // 15-Jan-2023, tatu: [databind#3736] Let's avoid detecting fields of Records // altogether (unless we find a good reason to detect them) - if (!isRecordType()) { + // 17-Apr-2023: Need Records' fields for serialization for cases like [databind#3895] & [databind#3628] + if (!isRecordType() || _forSerialization) { _addFields(props); // note: populates _fieldRenameMappings } _addMethods(props); diff --git a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsDeductionTypeSerializer.java b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsDeductionTypeSerializer.java index f23b574acaf..77762012352 100644 --- a/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsDeductionTypeSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/jsontype/impl/AsDeductionTypeSerializer.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo.As; import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.type.WritableTypeId; import com.fasterxml.jackson.databind.BeanProperty; @@ -39,9 +40,19 @@ public WritableTypeId writeTypePrefix(JsonGenerator g, // write surrounding Object or Array start/end markers. But // we are not to generate type id to write (compared to base class) - if (idMetadata.valueShape.isStructStart() - // also: do not try to write native type id - && !g.canWriteTypeId()) { + if (idMetadata.valueShape.isStructStart()) { + // 03-May-2023, tatu: [databind#3914]: should not write Native Type Id; + // but may need to write the value start marker + if (g.canWriteTypeId()) { + idMetadata.wrapperWritten = false; + if (idMetadata.valueShape == JsonToken.START_OBJECT) { + g.writeStartObject(idMetadata.forValue); + } else if (idMetadata.valueShape == JsonToken.START_ARRAY) { + g.writeStartArray(idMetadata.forValue); + } + return idMetadata; + } + // But for non-wrapper types can just use the default handling return g.writeTypePrefix(idMetadata); } return null; diff --git a/src/main/java/com/fasterxml/jackson/databind/module/SimpleModule.java b/src/main/java/com/fasterxml/jackson/databind/module/SimpleModule.java index 9632b26c8da..d8ba7293667 100644 --- a/src/main/java/com/fasterxml/jackson/databind/module/SimpleModule.java +++ b/src/main/java/com/fasterxml/jackson/databind/module/SimpleModule.java @@ -21,6 +21,13 @@ * as well as some other commonly * needed aspects (addition of custom {@link AbstractTypeResolver}s, * {@link com.fasterxml.jackson.databind.deser.ValueInstantiator}s). + *

+ * NOTE: that [de]serializers are registered as "default" [de]serializers. + * As a result, they will have lower priority than the ones indicated through annotations on + * both Class and property-associated annotations -- for example, + * {@link com.fasterxml.jackson.databind.annotation.JsonDeserialize}.
+ * In cases where both module-based [de]serializers and annotation-based [de]serializers are registered, + * the [de]serializer specified by the annotation will take precedence. *

* NOTE: although it is not expected that sub-types should need to * override {@link #setupModule(SetupContext)} method, if they choose @@ -312,6 +319,8 @@ protected SimpleModule setNamingStrategy(PropertyNamingStrategy naming) { *

* WARNING! "Last one wins" rule is applied. * Possible earlier addition of a serializer for a given Class will be replaced. + *

+ * NOTE: This method registers "default" (de)serializers only. See a note on precedence in class JavaDoc. */ public SimpleModule addSerializer(JsonSerializer ser) { @@ -325,6 +334,8 @@ public SimpleModule addSerializer(JsonSerializer ser) /** * Method for adding serializer to handle values of specific type. + *

+ * NOTE: This method registers "default" (de)serializers only. See a note on precedence in class JavaDoc. *

* WARNING! Type matching only uses type-erased {@code Class} and should NOT * be used when registering serializers for generic types like @@ -332,6 +343,8 @@ public SimpleModule addSerializer(JsonSerializer ser) *

* WARNING! "Last one wins" rule is applied. * Possible earlier addition of a serializer for a given Class will be replaced. + *

+ * NOTE: This method registers "default" (de)serializers only. See a note on precedence in class JavaDoc. */ public SimpleModule addSerializer(Class type, JsonSerializer ser) { @@ -344,6 +357,9 @@ public SimpleModule addSerializer(Class type, JsonSerializer return this; } + /** + * NOTE: This method registers "default" (de)serializers only. See a note on precedence in class JavaDoc. + */ public SimpleModule addKeySerializer(Class type, JsonSerializer ser) { _checkNotNull(type, "type to register key serializer for"); @@ -370,6 +386,8 @@ public SimpleModule addKeySerializer(Class type, JsonSerializer *

* WARNING! "Last one wins" rule is applied. * Possible earlier addition of a serializer for a given Class will be replaced. + *

+ * NOTE: This method registers "default" (de)serializers only. See a note on precedence in class JavaDoc. */ public SimpleModule addDeserializer(Class type, JsonDeserializer deser) { @@ -382,6 +400,9 @@ public SimpleModule addDeserializer(Class type, JsonDeserializer type, KeyDeserializer deser) { _checkNotNull(type, "type to register key deserializer for"); diff --git a/src/test-jdk14/java/com/fasterxml/jackson/databind/failing/RecordDeserialization3897Test.java b/src/test-jdk14/java/com/fasterxml/jackson/databind/failing/RecordDeserialization3897Test.java new file mode 100644 index 00000000000..c8273034405 --- /dev/null +++ b/src/test-jdk14/java/com/fasterxml/jackson/databind/failing/RecordDeserialization3897Test.java @@ -0,0 +1,23 @@ +package com.fasterxml.jackson.databind.failing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.BaseMapTest; + +// [databinding#3897] This is failing test for `Record` class deserialization with single field annotated with +// `JsonProperty.Access.WRITE_ONLY`. Regression from Jackson 2.14.2 +public class RecordDeserialization3897Test extends BaseMapTest { + + record Example( + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + String value + ) {} + + // Passes in 2.14.2, but does not in 2.15.0 + public void testRecordWithWriteOnly() throws Exception { + final String JSON = a2q("{'value':'foo'}"); + + Example result = newJsonMapper().readValue(JSON, Example.class); + + assertEquals("foo", result.value()); + } +} diff --git a/src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordIgnoreNonAccessorGetterTest.java b/src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordIgnoreNonAccessorGetterTest.java new file mode 100644 index 00000000000..af5077c2113 --- /dev/null +++ b/src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordIgnoreNonAccessorGetterTest.java @@ -0,0 +1,54 @@ +package com.fasterxml.jackson.databind.records; + +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.BaseMapTest; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class RecordIgnoreNonAccessorGetterTest extends BaseMapTest { + + // [databind#3628] + interface InterfaceWithGetter { + + String getId(); + + String getName(); + } + + @JsonPropertyOrder({"id", "name", "count"}) // easier to assert when JSON field ordering is always the same + record RecordWithInterfaceWithGetter(String name) implements InterfaceWithGetter { + + @Override + public String getId() { + return "ID:" + name; + } + + @Override + public String getName() { + return name; + } + + // [databind#3895] + public int getCount() { + return 999; + } + } + + private final ObjectMapper MAPPER = newJsonMapper(); + + public void testSerializeIgnoreInterfaceGetter_WithoutUsingVisibilityConfig() throws Exception { + String json = MAPPER.writeValueAsString(new RecordWithInterfaceWithGetter("Bob")); + + assertEquals("{\"id\":\"ID:Bob\",\"name\":\"Bob\",\"count\":999}", json); + } + + public void testSerializeIgnoreInterfaceGetter_UsingVisibilityConfig() throws Exception { + MAPPER.setVisibility(PropertyAccessor.GETTER, Visibility.NONE); + MAPPER.setVisibility(PropertyAccessor.FIELD, Visibility.ANY); + + String json = MAPPER.writeValueAsString(new RecordWithInterfaceWithGetter("Bob")); + + assertEquals("{\"name\":\"Bob\"}", json); + } +} diff --git a/src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordNullHandling3847Test.java b/src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordNullHandling3847Test.java index fcf287f9ede..f7f87f35903 100644 --- a/src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordNullHandling3847Test.java +++ b/src/test-jdk14/java/com/fasterxml/jackson/databind/records/RecordNullHandling3847Test.java @@ -23,6 +23,7 @@ static class Pojo3847 { } public record PlainRecord(String fieldName) {} + public record IntRecord(String description, int value) {} public record FixedRecord(@JsonProperty("field_name") String fieldName) {} @@ -101,4 +102,15 @@ public void testRecordFixerNullHandlingEmptyJson() throws Exception { verifyException(e, "Invalid `null` value encountered for property \"field_name\""); } } + + public void testRecordDefaultNullDeserialization() throws Exception { + PlainRecord pr = new ObjectMapper().readValue("{}", PlainRecord.class); + assertNull(pr.fieldName); + } + + public void testIntRecordDefaultNullDeserialization() throws Exception { + IntRecord ir = new ObjectMapper().readValue("{}", IntRecord.class); + assertNull(ir.description); + assertEquals(0, ir.value); + } } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/Issue3913DeserTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/Issue3913DeserTest.java new file mode 100644 index 00000000000..d7425cf93aa --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/deser/Issue3913DeserTest.java @@ -0,0 +1,88 @@ +package com.fasterxml.jackson.databind.deser; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.BaseMapTest; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.List; + +public class Issue3913DeserTest extends BaseMapTest +{ + // [databind#3913] + static class MyResponse { + List list; + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } + } + + interface Base { + + String getType(); + + String getMissingInJson(); + + @JsonCreator + static Base unmarshall( + @JsonProperty("missingInJson") String missingInJson, + @JsonProperty("type") String type + ) { + switch (type) { + case "impl": + return new Impl(type, missingInJson); + default: + return null; + } + } + } + + final static class Impl implements Base { + private String type; + private String missingInJson; + + public Impl() { + } + + public Impl(String type, String missingInJson) { + this.type = type; + this.missingInJson = missingInJson; + } + + @Override public String getType() { + return type; + } + + @Override public String getMissingInJson() { + return missingInJson; + } + + public void setType(String type) { + this.type = type; + } + + public void setMissingInJson(String missingInJson) { + this.missingInJson = missingInJson; + } + } + + // [databind#3913] + public void testDeserialization() throws JsonProcessingException { + ObjectMapper mapper = jsonMapperBuilder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + String rawResponse = "{\"list\":[{\"type\":\"impl\",\"unmappedKey\":\"unusedValue\"}]}"; + MyResponse myResponse = mapper.readValue(rawResponse, MyResponse.class); + assertNotNull(myResponse); + assertEquals(1, myResponse.list.size()); + assertEquals("impl", myResponse.list.get(0).getType()); + assertNull(myResponse.list.get(0).getMissingInJson()); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/TestBigNumbers.java b/src/test/java/com/fasterxml/jackson/databind/deser/TestBigNumbers.java index a173683cd45..3050e1a0118 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/TestBigNumbers.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/TestBigNumbers.java @@ -62,7 +62,7 @@ public void testDouble() throws Exception MAPPER.readValue(generateJson("d"), DoubleWrapper.class); fail("expected StreamReadException"); } catch (StreamConstraintsException e) { - verifyException(e, "Invalid numeric value ", "exceeds the maximum length"); + verifyException(e, "Number value length", "exceeds the maximum allowed"); } } @@ -79,7 +79,7 @@ public void testBigDecimal() throws Exception MAPPER.readValue(generateJson("number"), BigDecimalWrapper.class); fail("expected StreamReadException"); } catch (StreamConstraintsException e) { - verifyException(e, "Invalid numeric value ", "exceeds the maximum length"); + verifyException(e, "Number value length ", "exceeds the maximum allowed"); } } @@ -97,7 +97,7 @@ public void testBigInteger() throws Exception MAPPER.readValue(generateJson("number"), BigIntegerWrapper.class); fail("expected StreamReadException"); } catch (StreamConstraintsException e) { - verifyException(e, "Invalid numeric value ", "exceeds the maximum length"); + verifyException(e, "Number value length", "exceeds the maximum allowed"); } } diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/dos/StreamReadStringConstraintsTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/dos/StreamReadStringConstraintsTest.java index eea77178f35..d7c26882b49 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/dos/StreamReadStringConstraintsTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/dos/StreamReadStringConstraintsTest.java @@ -48,7 +48,7 @@ public void testBigString() throws Exception fail("expected JsonMappingException"); } catch (DatabindException e) { assertTrue("unexpected exception message: " + e.getMessage(), - e.getMessage().startsWith("String length (5001000) exceeds the maximum length (5000000)")); + e.getMessage().startsWith("String value length (5001000) exceeds the maximum allowed (5000000")); } } @@ -61,8 +61,8 @@ public void testBiggerString() throws Exception final String message = e.getMessage(); // this test fails when the TextBuffer is being resized, so we don't yet know just how big the string is // so best not to assert that the String length value in the message is the full 6000000 value - assertTrue("unexpected exception message: " + message, message.startsWith("String length")); - assertTrue("unexpected exception message: " + message, message.contains("exceeds the maximum length (5000000)")); + assertTrue("unexpected exception message: " + message, message.startsWith("String value length")); + assertTrue("unexpected exception message: " + message, message.contains("exceeds the maximum allowed (5000000")); } } diff --git a/src/test/java/com/fasterxml/jackson/databind/module/SimpleModuleAddMethodsTest.java b/src/test/java/com/fasterxml/jackson/databind/module/SimpleModuleAddMethodsTest.java new file mode 100644 index 00000000000..883a57f955c --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/module/SimpleModuleAddMethodsTest.java @@ -0,0 +1,239 @@ +package com.fasterxml.jackson.databind.module; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +/** + * Behavioral test to prove that by design decision, (de)serializers from class-level annotations will always + * have preference over default (de)serializers. + * + */ +@SuppressWarnings("serial") +public class SimpleModuleAddMethodsTest extends BaseMapTest +{ + @JsonDeserialize(using = ClassDogDeserializer.class) + @JsonSerialize(using = ClassDogSerializer.class) + static class Dog { + public String name; + + public Dog(String name) { + this.name = name; + } + } + + @JsonDeserialize(keyUsing = ClassDogKeyDeserializer.class, contentUsing = ClassDogDeserializer.class) + @JsonSerialize(keyUsing = ClassDogKeySerializer.class, contentUsing = ClassDogSerializer.class) + static class DogMap extends HashMap {} + + @JsonDeserialize(contentUsing = ClassDogDeserializer.class) + @JsonSerialize(contentUsing = ClassDogSerializer.class) + static class DogList extends ArrayList {} + + static class ClassDogSerializer extends JsonSerializer { + @Override + public void serialize(Dog value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString("class-dog"); + } + } + + static class ClassDogDeserializer extends JsonDeserializer { + @Override + public Dog deserialize(JsonParser p, DeserializationContext ctxt) { + return new Dog("class-dog"); + } + } + + static class ClassDogKeySerializer extends JsonSerializer { + @Override + public void serialize(Dog value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeFieldName("class-dog"); + } + } + + static class ClassDogKeyDeserializer extends KeyDeserializer { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) { + return new Dog("class-dog"); + } + } + + static class ModuleDogDeserializer extends JsonDeserializer { + @Override + public Dog deserialize(JsonParser p, DeserializationContext ctxt) { + return new Dog("module-dog"); + } + } + + static class ModuleDogSerializer extends JsonSerializer { + @Override + public void serialize(Dog value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString("module-dog"); + } + } + + static class ModuleDogKeyDeserializer extends KeyDeserializer { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) { + return new Dog("module-dog"); + } + } + + static class ModuleDogKeySerializer extends JsonSerializer { + @Override + public void serialize(Dog value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeFieldName("module-dog"); + } + } + + @JsonDeserialize(builder = SimpleBuilderXY.class) + static class BuildFailBean { + final int _x, _y; + + protected BuildFailBean(int x, int y) { + _x = x; + _y = y; + } + } + + @JsonDeserialize(builder = SimpleBuilderXY.class) + static class BuildSuccessBean { + final int _x, _y; + + protected BuildSuccessBean(int x, int y) { + _x = x; + _y = y; + } + } + + @JsonDeserialize(builder = java.lang.Void.class) + public static abstract class BuildBeanMixin {} + + static class SimpleBuilderXY { + public int x, y; + + public SimpleBuilderXY withX(int x0) { + this.x = x0; + return this; + } + + public SimpleBuilderXY withY(int y0) { + this.y = y0; + return this; + } + + public BuildFailBean build() { + return new BuildFailBean(x, y); + } + } + + static class BuildSuccessBeanDeserializer extends JsonDeserializer { + @Override + public BuildSuccessBean deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return new BuildSuccessBean(7, 8); + } + } + + /* + /********************************************************** + /* Test methods + /********************************************************** + */ + + private final ObjectMapper MAPPER = _buildMapper(); + + private ObjectMapper _buildMapper() { + ObjectMapper mapper = newJsonMapper(); + + // Simple tests + SimpleModule simpleModule = new SimpleModule() + .addSerializer(Dog.class, new ModuleDogSerializer()) + .addDeserializer(Dog.class, new ModuleDogDeserializer()) + .addKeyDeserializer(Dog.class, new ModuleDogKeyDeserializer()) + .addKeySerializer(Dog.class, new ModuleDogKeySerializer()); + + // "remove" builder annotation using mix-in + mapper.addMixIn(BuildFailBean.class, BuildBeanMixin.class); + + // "remove" builder annotation using mix-in, then register deserializer using module + mapper.addMixIn(BuildSuccessBean.class, BuildBeanMixin.class); + simpleModule.addDeserializer(BuildSuccessBean.class, new BuildSuccessBeanDeserializer()); + + mapper.registerModule(simpleModule); + return mapper; + } + + public void testPojoDeserialization() throws Exception { + Dog dog = MAPPER.readValue(a2q("{'name': 'my-dog'}"), Dog.class); + assertEquals("class-dog", dog.name); + } + + public void testPojoSerialization() throws Exception { + assertEquals( + a2q("'class-dog'"), + MAPPER.writeValueAsString(new Dog("my-dog"))); + } + + public void testRemoveAnnotationUsingMixIn() throws Exception { + try { + MAPPER.readValue( + a2q("{'x':1, 'y':2}"), BuildFailBean.class); + fail("Should not pass"); + } catch (InvalidDefinitionException e) { + verifyException(e, "cannot deserialize from Object value (no delegate- or property-based Creator)"); + } + } + + public void testRemoveAnnotationUsingMixInAndOverrideByModule() throws Exception { + BuildSuccessBean bean = MAPPER.readValue( + a2q("{'x':1, 'y':2}"), BuildSuccessBean.class); + assertEquals(7, bean._x); + assertEquals(8, bean._y); + } + + public void testDogMapDeserialization() throws Exception { + DogMap map = MAPPER.readValue(a2q("{'simple-dog': 'simple-dog'}"), DogMap.class); + + assertEquals(1, map.size()); + for (Map.Entry entry : map.entrySet()) { + assertEquals("class-dog", entry.getKey().name); + assertEquals("class-dog", entry.getValue().name); + } + } + + public void testDogMapSerialization() throws Exception { + DogMap map = new DogMap(); + map.put(new Dog("my-dog"), new Dog("my-dog")); + + assertEquals( + a2q("{'class-dog':'class-dog'}"), + MAPPER.writeValueAsString(map)); + } + + public void testDogListDeserialization() throws Exception { + DogList list = MAPPER.readValue(a2q("['simple-dog']"), DogList.class); + + assertEquals(1, list.size()); + for (Dog dog : list) { + assertEquals("class-dog", dog.name); + } + } + + public void testDogListSerialization() throws Exception { + DogList list = new DogList(); + list.add(new Dog("my-dog")); + + assertEquals( + a2q("['class-dog']"), + MAPPER.writeValueAsString(list)); + } +} +