From 97d2c951df2e259e52c5b688f696775e640a79ae Mon Sep 17 00:00:00 2001 From: Arnab Nandy Date: Wed, 12 Aug 2026 19:33:52 +0530 Subject: [PATCH] Add JsonPointer annotation for nested binding Signed-off-by: Arnab Nandy --- release-notes/VERSION-2.x | 2 + .../jackson/annotation/JsonPointer.java | 56 +++++++++++++++++++ .../jackson/annotation/JsonPointerTest.java | 56 +++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 src/main/java/com/fasterxml/jackson/annotation/JsonPointer.java create mode 100644 src/test/java/com/fasterxml/jackson/annotation/JsonPointerTest.java diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 8ab54ec8..df406bb3 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -18,6 +18,8 @@ NOTE: Jackson 3.x components rely on 2.x annotations; there are no separate #346: Add `@JsonWrapped` annotation (contributed by @sri-adarsh-kumar) +#357: Add `@JsonPointer` annotation for binding a property from a nested + JSON value selected with JSON Pointer syntax 2.22 (31-May-2026) diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonPointer.java b/src/main/java/com/fasterxml/jackson/annotation/JsonPointer.java new file mode 100644 index 00000000..73d5f03b --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonPointer.java @@ -0,0 +1,56 @@ +package com.fasterxml.jackson.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used to indicate that a property is to be deserialized from the + * value selected by a JSON Pointer expression, instead of from a direct child + * property of the containing JSON Object. + *

+ * For example, given input such as: + *

+ * {
+ *   "employee" : {
+ *     "details" : {
+ *       "departmentId" : 123
+ *     }
+ *   }
+ * }
+ *
+ * property can be bound directly with: + *
+ * public class Employee {
+ *   @JsonPointer("/employee/details/departmentId")
+ *   public int departmentId;
+ * }
+ *
+ * Pointer syntax follows JSON Pointer (RFC 6901), including escaping of + * {@code '~'} and {@code '/'} characters as {@code "~0"} and {@code "~1"}, + * respectively. + *

+ * This annotation only defines logical property access during deserialization; + * it has no effect on serialization. If the pointer does not resolve to a value, + * the property is considered absent. An explicitly resolved JSON {@code null} + * is handled as a regular null property value. + *

+ * Actual support for this annotation is provided by data-binding modules such + * as {@code jackson-databind}; annotation introspection by itself does not enable + * JSON Pointer traversal. + * + * @since 2.23 + */ +@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotation +public @interface JsonPointer +{ + /** + * JSON Pointer expression used to locate the value in the input document. + * + * @return JSON Pointer expression for the annotated property + */ + String value(); +} diff --git a/src/test/java/com/fasterxml/jackson/annotation/JsonPointerTest.java b/src/test/java/com/fasterxml/jackson/annotation/JsonPointerTest.java new file mode 100644 index 00000000..ede1ab8b --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/annotation/JsonPointerTest.java @@ -0,0 +1,56 @@ +package com.fasterxml.jackson.annotation; + +import java.lang.reflect.Constructor; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class JsonPointerTest +{ + private static class BeanWithField { + @JsonPointer("/nested/field") + public String field; + } + + private static class BeanWithMethod { + @JsonPointer("/nested/field") + public void setField(String field) { } + } + + private static class BeanWithParameter { + public BeanWithParameter(@JsonPointer("/nested/field") String field) { } + } + + @JsonPointer("/nested/field") + @JacksonAnnotationsInside + @interface BundleAnnotation { } + + @Test + public void testRuntimeRetentionOnField() throws Exception { + JsonPointer ann = BeanWithField.class.getField("field").getAnnotation(JsonPointer.class); + assertNotNull(ann); + assertEquals("/nested/field", ann.value()); + } + + @Test + public void testRuntimeRetentionOnMethod() throws Exception { + JsonPointer ann = BeanWithMethod.class.getMethod("setField", String.class) + .getAnnotation(JsonPointer.class); + assertNotNull(ann); + } + + @Test + public void testApplicableOnConstructorParameter() throws Exception { + Constructor ctor = BeanWithParameter.class.getDeclaredConstructor(String.class); + JsonPointer ann = ctor.getParameters()[0].getAnnotation(JsonPointer.class); + assertNotNull(ann); + } + + @Test + public void testApplicableOnAnnotationType() { + JsonPointer ann = BundleAnnotation.class.getAnnotation(JsonPointer.class); + assertNotNull(ann); + } +}