diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 0cdd1ac9d2..f8b5b7dfa9 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -8,6 +8,8 @@ Project: jackson-databind #2828: Add `DatabindException` as intermediate subtype of `JsonMappingException` #3001: Add mechanism for setting default `ContextAttributes` for `ObjectMapper` +#3002: Add `DeserializationContext.readTreeAsValue()` methods for more convenient + conversions for deserializers to use #3035: Add `removeMixIn()` method in `MapperBuilder` #3036: Backport `MapperBuilder` lambda-taking methods: `withConfigOverride()`, `withCoercionConfig()`, `withCoercionConfigDefaults()` diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java index 21f9e3e418..e419c3675f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java +++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationContext.java @@ -31,6 +31,7 @@ import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.TreeTraversingParser; import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.databind.util.*; @@ -912,6 +913,10 @@ public T readValue(JsonParser p, JavaType type) throws IOException { * for reading one-off values for the composite type, taking into account * annotations that the property (passed to this method -- usually property that * has custom serializer that called this method) has. + * + * @param p Parser that points to the first token of the value to read + * @param prop Logical property of a POJO being type + * @return Value of type {@code type} that was read * * @since 2.4 */ @@ -920,6 +925,10 @@ public T readPropertyValue(JsonParser p, BeanProperty prop, Class type) t } /** + * Same as {@link #readPropertyValue(JsonParser, BeanProperty, Class)} but with + * fully resolved {@link JavaType} as target: needs to be used for generic types, + * for example. + * * @since 2.4 */ @SuppressWarnings("unchecked") @@ -934,6 +943,13 @@ public T readPropertyValue(JsonParser p, BeanProperty prop, JavaType type) t } /** + * Convenience method for reading the value that passed {@link JsonParser} + * points to as a {@link JsonNode}. + * + * @param p Parser that points to the first token of the value to read + * + * @return Value read as {@link JsonNode} + * * @since 2.10 */ public JsonNode readTree(JsonParser p) throws IOException { @@ -951,6 +967,71 @@ public JsonNode readTree(JsonParser p) throws IOException { .deserialize(p, this); } + /** + * Helper method similar to {@link ObjectReader#treeToValue(TreeNode, Class)} + * which will read contents of given tree ({@link JsonNode}) + * and bind them into specified target type. This is often used in two-phase + * deserialization in which content is first read as a tree, then manipulated + * (adding and/or removing properties of Object values, for example), + * and finally converted into actual target type using default deserialization + * logic for the type. + *

+ * NOTE: deserializer implementations should be careful not to try to recursively + * deserialize into target type deserializer has registered itself to handle. + * + * @param n Tree value to convert, if not {@code null}: if {@code null}, will simply + * return {@code null} + * @param targetType Type to deserialize contents of {@code n} into (if {@code n} not {@code null}) + * + * @return Either {@code null} (if {@code n} was {@code null} or a value of + * type {@code type} that was read from non-{@code null} {@code n} argument + * + * @since 2.13 + */ + public T readTreeAsValue(JsonNode n, Class targetType) throws IOException + { + if (n == null) { + return null; + } + try (TreeTraversingParser p = _treeAsTokens(n)) { + return readValue(p, targetType); + } + } + + /** + * Same as {@link #readTreeAsValue(JsonNode, Class)} but will fully resolved + * {@link JavaType} as {@code targetType} + *

+ * NOTE: deserializer implementations should be careful not to try to recursively + * deserialize into target type deserializer has registered itself to handle. + * + * @param n Tree value to convert + * @param targetType Type to deserialize contents of {@code n} into + * + * @return Value of type {@code type} that was read + * + * @since 2.13 + */ + public T readTreeAsValue(JsonNode n, JavaType targetType) throws IOException + { + if (n == null) { + return null; + } + try (TreeTraversingParser p = _treeAsTokens(n)) { + return readValue(p, targetType); + } + } + + private TreeTraversingParser _treeAsTokens(JsonNode n) throws IOException + { + // Not perfect but has to do... + ObjectCodec codec = (_parser == null) ? null : _parser.getCodec(); + TreeTraversingParser p = new TreeTraversingParser(n, codec); + // important: must initialize... + p.nextToken(); + return p; + } + /* /********************************************************** /* Methods for problem handling diff --git a/src/main/java/com/fasterxml/jackson/databind/ObjectReader.java b/src/main/java/com/fasterxml/jackson/databind/ObjectReader.java index ab3d01bb6a..56d7ad836a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ObjectReader.java +++ b/src/main/java/com/fasterxml/jackson/databind/ObjectReader.java @@ -1973,7 +1973,20 @@ public T treeToValue(TreeNode n, Class valueType) throws JsonProcessingEx } catch (IOException e) { // should not occur, no real i/o... throw JsonMappingException.fromUnexpectedIOE(e); } - } + } + + // @since 2.13 + public T treeToValue(TreeNode n, JavaType valueType) throws JsonProcessingException + { + _assertNotNull("n", n); + try { + return readValue(treeAsTokens(n), valueType); + } catch (JsonProcessingException e) { + throw e; + } catch (IOException e) { // should not occur, no real i/o... + throw JsonMappingException.fromUnexpectedIOE(e); + } + } @Override public void writeValue(JsonGenerator gen, Object value) throws IOException { diff --git a/src/test/java/com/fasterxml/jackson/databind/ObjectReaderTest.java b/src/test/java/com/fasterxml/jackson/databind/ObjectReaderTest.java index c509c56d88..788498f82c 100644 --- a/src/test/java/com/fasterxml/jackson/databind/ObjectReaderTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/ObjectReaderTest.java @@ -447,8 +447,13 @@ public void testTreeToValue() throws Exception ObjectReader r = MAPPER.readerFor(String.class); List list = r.treeToValue(n, List.class); assertEquals(1, list.size()); + + // since 2.13: + String[] arr = r.treeToValue(n, MAPPER.constructType(String[].class)); + assertEquals(1, arr.length); + assertEquals("xyz", arr[0]); } - + public void testCodecUnsupportedWrites() throws Exception { ObjectReader r = MAPPER.readerFor(String.class); diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/TestCustomDeserializers.java b/src/test/java/com/fasterxml/jackson/databind/deser/TestCustomDeserializers.java index eb1601ab92..887b621ee0 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/TestCustomDeserializers.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/TestCustomDeserializers.java @@ -313,13 +313,42 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) } } + @JsonDeserialize(using = NamedPointDeserializer.class) + static class NamedPoint + { + public Point point; + public String name; + + public NamedPoint(String name, Point point) { + this.point = point; + this.name = name; + } + } + + static class NamedPointDeserializer extends StdDeserializer + { + public NamedPointDeserializer() { + super(NamedPoint.class); + } + + @Override + public NamedPoint deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException + { + JsonNode tree = ctxt.readTree(p); + String name = tree.path("name").asText(null); + Point point = ctxt.readTreeAsValue(tree.get("point"), Point.class); + return new NamedPoint(name, point); + } + } + /* /********************************************************** /* Unit tests /********************************************************** */ - final ObjectMapper MAPPER = objectMapper(); + private final ObjectMapper MAPPER = newJsonMapper(); public void testCustomBeanDeserializer() throws Exception { @@ -388,7 +417,6 @@ public Immutable convert(JsonNode value) // [databind#623] public void testJsonNodeDelegating() throws Exception { - ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule("test", Version.unknownVersion()); module.addDeserializer(Immutable.class, new StdNodeBasedDeserializer(Immutable.class) { @@ -399,7 +427,9 @@ public Immutable convert(JsonNode root, DeserializationContext ctxt) throws IOEx return new Immutable(x, y); } }); - mapper.registerModule(module); + ObjectMapper mapper = jsonMapperBuilder() + .addModule(module) + .build(); Immutable imm = mapper.readValue("{\"x\":-10,\"y\":3}", Immutable.class); assertEquals(-10, imm.x); assertEquals(3, imm.y); @@ -485,7 +515,7 @@ public JsonDeserializer modifyDeserializer(DeserializationConfig config, } // [databind#2452] - public void testCustomSerializerWithReadTree() throws Exception + public void testCustomDeserializerWithReadTree() throws Exception { ObjectMapper mapper = jsonMapperBuilder() .addModule(new SimpleModule() @@ -499,4 +529,26 @@ public void testCustomSerializerWithReadTree() throws Exception assertEquals(3, n.size()); assertEquals(123, n.get(2).intValue()); } + + // [databind#3002] + public void testCustomDeserializerWithReadTreeAsValue() throws Exception + { + final String json = a2q("{'point':{'x':13, 'y':-4}, 'name':'Foozibald' }"); + NamedPoint result = MAPPER.readValue(json, NamedPoint.class); + assertNotNull(result); + assertEquals("Foozibald", result.name); + assertEquals(new Point(13, -4), result.point); + + // and with JavaType variant too + result = MAPPER.readValue(json, MAPPER.constructType(NamedPoint.class)); + assertNotNull(result); + assertEquals("Foozibald", result.name); + assertEquals(new Point(13, -4), result.point); + + // also, try some edge conditions + result = MAPPER.readValue(a2q("{'name':4})"), NamedPoint.class); + assertNotNull(result); + assertEquals("4", result.name); + assertNull(result.point); + } }