diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java index ff060f4f5d..39a00f961f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java @@ -16,10 +16,7 @@ import com.fasterxml.jackson.databind.cfg.ConstructorDetector; import com.fasterxml.jackson.databind.cfg.DeserializerFactoryConfig; import com.fasterxml.jackson.databind.cfg.HandlerInstantiator; -import com.fasterxml.jackson.databind.deser.impl.CreatorCandidate; -import com.fasterxml.jackson.databind.deser.impl.CreatorCollector; -import com.fasterxml.jackson.databind.deser.impl.JDKValueInstantiators; -import com.fasterxml.jackson.databind.deser.impl.JavaUtilCollectionsDeserializers; +import com.fasterxml.jackson.databind.deser.impl.*; import com.fasterxml.jackson.databind.deser.std.*; import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; import com.fasterxml.jackson.databind.ext.OptionalHandlerFactory; @@ -52,12 +49,6 @@ public abstract class BasicDeserializerFactory private final static Class CLASS_MAP_ENTRY = Map.Entry.class; private final static Class CLASS_SERIALIZABLE = Serializable.class; - /** - * We need a placeholder for creator properties that don't have name - * but are marked with `@JsonWrapped` annotation. - */ - protected final static PropertyName UNWRAPPED_CREATOR_PARAM_NAME = new PropertyName("@JsonUnwrapped"); - /* /********************************************************** /* Config @@ -556,11 +547,8 @@ protected void _addImplicitConstructorCreators(DeserializationContext ctxt, } NameTransformer unwrapper = intr.findUnwrappingNameTransformer(param); if (unwrapper != null) { - _reportUnwrappedCreatorProperty(ctxt, beanDesc, param); - /* - properties[i] = constructCreatorProperty(ctxt, beanDesc, UNWRAPPED_CREATOR_PARAM_NAME, i, param, null); + properties[i] = constructCreatorProperty(ctxt, beanDesc, UnwrappedPropertyHandler.UNWRAPPED_CREATOR_PARAM_NAME, i, param, null); ++explicitNameCount; - */ continue; } // One more thing: implicit names are ok iff ctor has creator annotation @@ -738,11 +726,8 @@ protected void _addImplicitFactoryCreators(DeserializationContext ctxt, } NameTransformer unwrapper = intr.findUnwrappingNameTransformer(param); if (unwrapper != null) { - _reportUnwrappedCreatorProperty(ctxt, beanDesc, param); - /* - properties[i] = constructCreatorProperty(ctxt, beanDesc, UNWRAPPED_CREATOR_PARAM_NAME, i, param, null); + properties[i] = constructCreatorProperty(ctxt, beanDesc, UnwrappedPropertyHandler.UNWRAPPED_CREATOR_PARAM_NAME, i, param, null); ++implicitNameCount; - */ continue; } // One more thing: implicit names are ok iff ctor has creator annotation @@ -872,11 +857,8 @@ protected void _addExplicitPropertyCreator(DeserializationContext ctxt, // as that will not work with Creators well at all NameTransformer unwrapper = ctxt.getAnnotationIntrospector().findUnwrappingNameTransformer(param); if (unwrapper != null) { - _reportUnwrappedCreatorProperty(ctxt, beanDesc, param); - /* - properties[i] = constructCreatorProperty(ctxt, beanDesc, UNWRAPPED_CREATOR_PARAM_NAME, i, param, null); - ++explicitNameCount; - */ + properties[i] = constructCreatorProperty(ctxt, beanDesc, UnwrappedPropertyHandler.UNWRAPPED_CREATOR_PARAM_NAME, i, param, null); + continue; } name = candidate.findImplicitParamName(i); _validateNamedPropertyParameter(ctxt, beanDesc, candidate, i, @@ -1168,14 +1150,12 @@ protected void _validateNamedPropertyParameter(DeserializationContext ctxt, } } - // 01-Dec-2016, tatu: As per [databind#265] we cannot yet support passing - // of unwrapped values through creator properties, so fail fast - protected void _reportUnwrappedCreatorProperty(DeserializationContext ctxt, - BeanDescription beanDesc, AnnotatedParameter param) - throws JsonMappingException + protected void _reportUnnamedUnwrappedCreatorProperty(DeserializationContext ctxt, + BeanDescription beanDesc, AnnotatedParameter param) + throws JsonMappingException { ctxt.reportBadTypeDefinition(beanDesc, -"Cannot define Creator parameter %d as `@JsonUnwrapped`: combination not yet supported", + "Cannot define Creator parameter %d as `@JsonUnwrapped` without also specifying `@JsonProperty`", param.getIndex()); } 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 6fd0c3151f..ee77bf58d5 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializer.java @@ -848,7 +848,7 @@ protected Object deserializeUsingPropertyBasedWithUnwrapped(JsonParser p, Deseri if (buffer.readIdProperty(propName) && creatorProp == null) { continue; } - if (creatorProp != null) { + if (creatorProp != null && !_unwrappedPropertyHandler.isUnwrapped(creatorProp)) { // Last creator property to set? if (buffer.assignParameter(creatorProp, _deserializeWithErrorWrapping(p, ctxt, creatorProp))) { @@ -919,6 +919,10 @@ protected Object deserializeUsingPropertyBasedWithUnwrapped(JsonParser p, Deseri } } + // We could still have some unset creator properties that are unwrapped. These have to be processed last, because 'tokens' contains + // all the properties that remain after regular deserialization. + buffer = _unwrappedPropertyHandler.processUnwrappedCreatorProperties(p, ctxt, buffer, tokens); + // We hit END_OBJECT, so: Object bean; try { 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 aa0c3fd1a2..f1012c488f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BeanDeserializerBase.java @@ -577,7 +577,13 @@ public void resolve(DeserializationContext ctxt) throws JsonMappingException if (unwrapped == null) { unwrapped = new UnwrappedPropertyHandler(); } - unwrapped.addProperty(prop); + + if (prop instanceof CreatorProperty) { + unwrapped.addCreatorProperty(prop); + } else { + unwrapped.addProperty(prop); + } + // 12-Dec-2014, tatu: As per [databind#647], we will have problems if // the original property is left in place. So let's remove it now. // 25-Mar-2017, tatu: Wonder if this could be problematic wrt creators? @@ -1010,13 +1016,6 @@ protected NameTransformer _findPropertyUnwrapper(DeserializationContext ctxt, if (am != null) { NameTransformer unwrapper = ctxt.getAnnotationIntrospector().findUnwrappingNameTransformer(am); if (unwrapper != null) { - // 01-Dec-2016, tatu: As per [databind#265] we cannot yet support passing - // of unwrapped values through creator properties, so fail fast - if (prop instanceof CreatorProperty) { - ctxt.reportBadDefinition(getValueType(), String.format( - "Cannot define Creator property \"%s\" as `@JsonUnwrapped`: combination not yet supported", - prop.getName())); - } return unwrapper; } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/UnwrappedPropertyHandler.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/UnwrappedPropertyHandler.java index 87846de9f4..a1611d55e7 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/UnwrappedPropertyHandler.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/UnwrappedPropertyHandler.java @@ -1,62 +1,112 @@ package com.fasterxml.jackson.databind.deser.impl; -import java.io.IOException; -import java.util.*; - -import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.PropertyName; import com.fasterxml.jackson.databind.deser.SettableBeanProperty; import com.fasterxml.jackson.databind.util.NameTransformer; import com.fasterxml.jackson.databind.util.TokenBuffer; +import java.io.IOException; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + /** * Object that is responsible for handling acrobatics related to * deserializing "unwrapped" values; sets of properties that are * embedded (inlined) as properties of parent JSON object. */ -public class UnwrappedPropertyHandler -{ +public class UnwrappedPropertyHandler { + /** + * We need a placeholder for creator properties that don't have name + * but are marked with `@JsonWrapped` annotation. + */ + public final static PropertyName UNWRAPPED_CREATOR_PARAM_NAME = new PropertyName("@JsonUnwrapped"); + + protected final List _creatorProperties; protected final List _properties; + protected final Set _unwrappedPropertyNames; + + public UnwrappedPropertyHandler() { + _creatorProperties = new ArrayList<>(); + _properties = new ArrayList<>(); + _unwrappedPropertyNames = new HashSet<>(); + } - public UnwrappedPropertyHandler() { - _properties = new ArrayList(); - } - protected UnwrappedPropertyHandler(List props) { + protected UnwrappedPropertyHandler(List creatorProps, List props) { + _creatorProperties = creatorProps; _properties = props; + _unwrappedPropertyNames = Stream.concat(creatorProps.stream(), props.stream()) + .map(SettableBeanProperty::getName) + .collect(Collectors.toSet()); + } + + public void addCreatorProperty(SettableBeanProperty property) { + _creatorProperties.add(property); + _unwrappedPropertyNames.add(property.getName()); } public void addProperty(SettableBeanProperty property) { _properties.add(property); + _unwrappedPropertyNames.add(property.getName()); } - public UnwrappedPropertyHandler renameAll(NameTransformer transformer) - { - ArrayList newProps = new ArrayList(_properties.size()); - for (SettableBeanProperty prop : _properties) { + public UnwrappedPropertyHandler renameAll(NameTransformer transformer) { + return new UnwrappedPropertyHandler( + renameProperties(_creatorProperties, transformer), + renameProperties(_properties, transformer) + ); + } + + private List renameProperties( + Collection properties, + NameTransformer transformer + ) { + List newProps = new ArrayList(properties.size()); + for (SettableBeanProperty prop : properties) { String newName = transformer.transform(prop.getName()); prop = prop.withSimpleName(newName); JsonDeserializer deser = prop.getValueDeserializer(); if (deser != null) { @SuppressWarnings("unchecked") - JsonDeserializer newDeser = (JsonDeserializer) - deser.unwrappingDeserializer(transformer); + JsonDeserializer newDeser = (JsonDeserializer) deser.unwrappingDeserializer(transformer); if (newDeser != deser) { prop = prop.withValueDeserializer(newDeser); } } newProps.add(prop); } - return new UnwrappedPropertyHandler(newProps); + return newProps; + } + + public boolean isUnwrapped(SettableBeanProperty property) { + return this._unwrappedPropertyNames.contains(property.getName()); + } + + public PropertyValueBuffer processUnwrappedCreatorProperties( + JsonParser originalParser, + DeserializationContext ctxt, + PropertyValueBuffer values, + TokenBuffer buffered + ) throws IOException { + for (SettableBeanProperty prop : _creatorProperties) { + JsonParser p = buffered.asParser(originalParser.streamReadConstraints()); + p.nextToken(); + Object deserialized = prop.deserialize(p, ctxt); + values.assignParameter(prop, deserialized); + } + + return values; } - @SuppressWarnings("resource") public Object processUnwrapped(JsonParser originalParser, DeserializationContext ctxt, - Object bean, TokenBuffer buffered) - throws IOException - { - for (int i = 0, len = _properties.size(); i < len; ++i) { - SettableBeanProperty prop = _properties.get(i); + Object bean, TokenBuffer buffered) + throws IOException { + for (SettableBeanProperty prop : _properties) { JsonParser p = buffered.asParser(originalParser.streamReadConstraints()); p.nextToken(); prop.deserializeAndSet(p, ctxt, bean); 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 61961db4db..9ffb099688 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.cfg.HandlerInstantiator; import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.deser.impl.UnwrappedPropertyHandler; import com.fasterxml.jackson.databind.jdk14.JDK14Util; import com.fasterxml.jackson.databind.util.ClassUtil; @@ -716,6 +717,14 @@ private void _addCreatorParam(Map props, PropertyName pn = _annotationIntrospector.findNameForDeserialization(param); boolean expl = (pn != null && !pn.isEmpty()); if (!expl) { + boolean unrwapping = _annotationIntrospector.findUnwrappingNameTransformer(param) != null; + if (unrwapping) { + POJOPropertyBuilder prop = _property(props, UnwrappedPropertyHandler.UNWRAPPED_CREATOR_PARAM_NAME); + prop.addCtor(param, UnwrappedPropertyHandler.UNWRAPPED_CREATOR_PARAM_NAME, false, true, false); + _creatorProperties.add(prop); + return; + } + if (impl.isEmpty()) { // Important: if neither implicit nor explicit name, cannot make use of // this creator parameter -- may or may not be a problem, verified at a later point. diff --git a/src/test/java/com/fasterxml/jackson/databind/struct/TestUnwrappedWithJsonCreator.java b/src/test/java/com/fasterxml/jackson/databind/struct/TestUnwrappedWithJsonCreator.java new file mode 100644 index 0000000000..64c7fdc518 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/struct/TestUnwrappedWithJsonCreator.java @@ -0,0 +1,128 @@ +package com.fasterxml.jackson.databind.struct; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.*; + +/** + * Tests to verify [databind#1467]. + */ +public class TestUnwrappedWithJsonCreator extends BaseMapTest +{ + + static class ExplicitWithoutName { + private final String unrelated; + private final Inner inner; + + @JsonCreator + public ExplicitWithoutName(@JsonProperty("unrelated") String unrelated, @JsonUnwrapped Inner inner) { + this.unrelated = unrelated; + this.inner = inner; + } + + public String getUnrelated() { + return unrelated; + } + + @JsonUnwrapped + public Inner getInner() { + return inner; + } + } + + static class ExplicitWithName { + private final String unrelated; + private final Inner inner; + + @JsonCreator + public ExplicitWithName(@JsonProperty("unrelated") String unrelated, @JsonProperty("inner") @JsonUnwrapped Inner inner) { + this.unrelated = unrelated; + this.inner = inner; + } + + public String getUnrelated() { + return unrelated; + } + + public Inner getInner() { + return inner; + } + } + + static class ImplicitWithName { + private final String unrelated; + private final Inner inner; + + public ImplicitWithName(@JsonProperty("unrelated") String unrelated, @JsonProperty("inner") @JsonUnwrapped Inner inner) { + this.unrelated = unrelated; + this.inner = inner; + } + + public String getUnrelated() { + return unrelated; + } + + public Inner getInner() { + return inner; + } + } + + static class Inner { + private final String property1; + private final String property2; + + public Inner(@JsonProperty("property1") String property1, @JsonProperty("property2") String property2) { + this.property1 = property1; + this.property2 = property2; + } + + public String getProperty1() { + return property1; + } + + public String getProperty2() { + return property2; + } + } + + /* + /********************************************************** + /* Tests, deserialization + /********************************************************** + */ + + public void testUnwrappedWithJsonCreatorWithExplicitWithoutName() throws Exception + { + ObjectMapper mapper = new ObjectMapper(); + + String json = "{\"unrelated\": \"unrelatedValue\", \"property1\": \"value1\", \"property2\": \"value2\"}"; + ExplicitWithoutName outer = mapper.readValue(json, ExplicitWithoutName.class); + + assertEquals("unrelatedValue", outer.getUnrelated()); + assertEquals("value1", outer.getInner().getProperty1()); + assertEquals("value2", outer.getInner().getProperty2()); + } + + public void testUnwrappedWithJsonCreatorExplicitWithName() throws Exception + { + ObjectMapper mapper = new ObjectMapper(); + + String json = "{\"unrelated\": \"unrelatedValue\", \"property1\": \"value1\", \"property2\": \"value2\"}"; + ExplicitWithName outer = mapper.readValue(json, ExplicitWithName.class); + + assertEquals("unrelatedValue", outer.getUnrelated()); + assertEquals("value1", outer.getInner().getProperty1()); + assertEquals("value2", outer.getInner().getProperty2()); + } + + public void testUnwrappedWithJsonCreatorImplicitWithName() throws Exception + { + ObjectMapper mapper = new ObjectMapper(); + + String json = "{\"unrelated\": \"unrelatedValue\", \"property1\": \"value1\", \"property2\": \"value2\"}"; + ImplicitWithName outer = mapper.readValue(json, ImplicitWithName.class); + + assertEquals("unrelatedValue", outer.getUnrelated()); + assertEquals("value1", outer.getInner().getProperty1()); + assertEquals("value2", outer.getInner().getProperty2()); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/struct/UnwrappedCreatorParam265Test.java b/src/test/java/com/fasterxml/jackson/databind/struct/UnwrappedCreatorParam265Test.java deleted file mode 100644 index 494205ea5f..0000000000 --- a/src/test/java/com/fasterxml/jackson/databind/struct/UnwrappedCreatorParam265Test.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.fasterxml.jackson.databind.struct; - -import com.fasterxml.jackson.annotation.*; - -import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; - -public class UnwrappedCreatorParam265Test extends BaseMapTest -{ - static class JAddress { - public String address; - public String city; - public String state; - - protected JAddress() { } - - public JAddress(String address, String city, String state) { - this.address = address; - this.city = city; - this.state = state; - } - } - - static class JPersonWithoutName - { - public String name; - - protected JAddress _address; - - @JsonCreator - public JPersonWithoutName(@JsonProperty("name") String name, - @JsonUnwrapped JAddress address) - { - this.name = name; - _address = address; - } - - @JsonUnwrapped - public JAddress getAddress() { return _address; } - } - - static class JPersonWithName - { - public String name; - - protected JAddress _address; - - @JsonCreator - public JPersonWithName(@JsonProperty("name") String name, - @JsonUnwrapped - @JsonProperty("address") - JAddress address) - { - this.name = name; - _address = address; - } - - @JsonUnwrapped - public JAddress getAddress() { return _address; } - } - - /* - /********************************************************** - /* Test methods - /********************************************************** - */ - - // For [databind#265]: handle problem by throwing exception - public void testUnwrappedWithUnnamedCreatorParam() throws Exception - { - JPersonWithoutName person = new JPersonWithoutName("MyName", new JAddress("main street", "springfield", "WA")); - ObjectMapper mapper = new ObjectMapper(); - // serialization should be fine as far as that goes - String json = mapper.writeValueAsString(person); - - // but not deserialization: - try { - /*JPersonWithoutName result =*/ mapper.readValue(json, JPersonWithoutName.class); - fail("Should not pass"); - } catch (InvalidDefinitionException e) { - verifyException(e, "Cannot define Creator parameter"); - verifyException(e, "@JsonUnwrapped"); - } - } - - // For [databind#265]: handle problem by throwing exception - public void testUnwrappedWithNamedCreatorParam() throws Exception - { - JPersonWithName person = new JPersonWithName("MyName", new JAddress("main street", "springfield", "WA")); - ObjectMapper mapper = new ObjectMapper(); - // serialization should be fine as far as that goes - String json = mapper.writeValueAsString(person); - try { - /*JPersonWithName result =*/ mapper.readValue(json, JPersonWithName.class); - fail("Should not pass"); - } catch (InvalidDefinitionException e) { - verifyException(e, "Cannot define Creator property \"address\""); - verifyException(e, "@JsonUnwrapped"); - } - } -}