From 61cf530134152f72517318326b65120af2b06c84 Mon Sep 17 00:00:00 2001 From: Sri Adarsh Kumar Date: Wed, 29 Apr 2026 16:02:40 +0200 Subject: [PATCH 1/3] Add @JsonWrapped annotation (moved from jackson-databind) Co-Authored-By: Claude Haiku 4.5 --- release-notes/VERSION-2.x | 2 + .../jackson/annotation/JsonWrapped.java | 74 +++++++++++++++++++ .../jackson/annotation/JsonWrappedTest.java | 61 +++++++++++++++ 3 files changed, 137 insertions(+) create mode 100644 src/main/java/com/fasterxml/jackson/annotation/JsonWrapped.java create mode 100644 src/test/java/com/fasterxml/jackson/annotation/JsonWrappedTest.java diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 5464a3af..80d00c0c 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -23,6 +23,8 @@ NOTE: Jackson 3.x components rely on 2.x annotations; there are no separate #342: Add `@JsonTypeInfo.writeTypeIdForDefaultImpl` to allow skipping writing of type id for values of default type #344: Improve `Locale` handling in `JsonFormat.Value` +#512: Add `@JsonWrapped` annotation for grouping bean properties into a + nested JSON object (inverse of `@JsonUnwrapped`) 2.21 (18-Jan-2026) diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonWrapped.java b/src/main/java/com/fasterxml/jackson/annotation/JsonWrapped.java new file mode 100644 index 00000000..3447ac2c --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonWrapped.java @@ -0,0 +1,74 @@ +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 that groups one or more bean properties into a synthetic + * nested JSON object during serialization, and extracts them back during + * deserialization. This is the inverse of {@link JsonUnwrapped}. + * + *

Multiple fields annotated with the same {@code value()} are grouped into + * a single wrapper object. Inner property names follow Jackson's standard naming + * ({@code @JsonProperty} or default). + * + *

Example: given a POJO such as: + *

+ * public class Gene {
+ *     public String name;
+ *
+ *     @JsonWrapped("chr")
+ *     public String chromosome;
+ *
+ *     @JsonWrapped("chr")
+ *     public int position;
+ * }
+ * 
+ * serialization produces: + *
+ * {
+ *   "name" : "BRCA1",
+ *   "chr" : {
+ *     "chromosome" : "17",
+ *     "position" : 43044295
+ *   }
+ * }
+ * 
+ * + *

Constraints: + *

+ * + * @see JsonUnwrapped + * @since 2.22 + */ +@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotation +public @interface JsonWrapped { + /** + * Single-level wrapper object name (e.g. "chr"). + * An empty string disables wrapping (useful in mix-ins to suppress + * wrapping defined in a supertype). + */ + String value(); +} diff --git a/src/test/java/com/fasterxml/jackson/annotation/JsonWrappedTest.java b/src/test/java/com/fasterxml/jackson/annotation/JsonWrappedTest.java new file mode 100644 index 00000000..ce6522d5 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/annotation/JsonWrappedTest.java @@ -0,0 +1,61 @@ +package com.fasterxml.jackson.annotation; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class JsonWrappedTest + extends AnnotationTestUtil +{ + private static class TestClass { + @JsonWrapped("chr") + public String chromosome; + + @JsonWrapped("chr") + public int position; + } + + private static class TestClassWithGetter { + private String data; + + @JsonWrapped("wrapper") + public String getData() { + return data; + } + } + + private static class TestClassEmptyWrapper { + @JsonWrapped("") + public String field; + } + + @Test + public void testAnnotationRetentionAtRuntime() throws Exception { + // Verify annotation is retained at runtime + JsonWrapped ann = TestClass.class.getField("chromosome").getAnnotation(JsonWrapped.class); + assertNotNull(ann, "Annotation should be retained at runtime"); + assertEquals("chr", ann.value()); + } + + @Test + public void testAnnotationValue() throws Exception { + JsonWrapped ann = TestClass.class.getField("chromosome").getAnnotation(JsonWrapped.class); + assertEquals("chr", ann.value()); + + JsonWrapped ann2 = TestClass.class.getField("position").getAnnotation(JsonWrapped.class); + assertEquals("chr", ann2.value()); + } + + @Test + public void testEmptyStringValue() throws Exception { + JsonWrapped ann = TestClassEmptyWrapper.class.getField("field").getAnnotation(JsonWrapped.class); + assertEquals("", ann.value()); + } + + @Test + public void testAnnotationOnMethod() throws Exception { + JsonWrapped ann = TestClassWithGetter.class.getMethod("getData").getAnnotation(JsonWrapped.class); + assertNotNull(ann, "Annotation should be applicable to methods"); + assertEquals("wrapper", ann.value()); + } +} From 1ca94120b66aed00e50caaeeccb4f4a7c813c848 Mon Sep 17 00:00:00 2001 From: Sri Adarsh Kumar Date: Fri, 1 May 2026 11:17:23 +0200 Subject: [PATCH 2/3] feat(JsonWrapped): add `enabled` flag and simplify tests - Add `boolean enabled() default true` to @JsonWrapped following the @JsonUnwrapped pattern, to allow mix-in annotations to disable wrapping - Add ElementType.ANNOTATION_TYPE to @Target (matches @JsonUnwrapped) - Update Javadoc to document enabled=false as the canonical override idiom - Replace tests with structural/retention checks covering Field, Method, Constructor Parameter, and Annotation targets (per maintainer feedback) Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../jackson/annotation/JsonWrapped.java | 16 +++-- .../jackson/annotation/JsonWrappedTest.java | 60 ++++++++----------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonWrapped.java b/src/main/java/com/fasterxml/jackson/annotation/JsonWrapped.java index 3447ac2c..ed111abc 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JsonWrapped.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonWrapped.java @@ -44,9 +44,10 @@ * its own name within the wrapper object. Note: existing interaction limitations * around {@code @JsonView}, {@code @JsonFilter}, and {@code @JsonInclude} on * inner wrapped fields still apply — see the remaining bullets below. - *
  • The wrapper name ({@code value()}) must be non-empty, unless explicitly disabling - * wrapping: an empty {@code value()} ({@code @JsonWrapped("")}) disables wrapping — - * useful in mix-ins to suppress wrapping defined in a supertype.
  • + *
  • To disable wrapping via a mix-in annotation, use {@code enabled=false} + * (e.g. {@code @JsonWrapped(value="name", enabled=false)}); this is the standard + * Jackson override idiom. Alternatively, an empty {@code value()} ({@code @JsonWrapped("")}) + * also disables wrapping.
  • *
  • The wrapper name must not conflict with an existing non-wrapped property on the same bean.
  • *
  • Not supported on {@code @JsonCreator} constructor or factory-method parameters.
  • *
  • MVP limitation: {@code @JsonView} on inner wrapped fields is ignored — the wrapper @@ -61,7 +62,7 @@ * @see JsonUnwrapped * @since 2.22 */ -@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotation public @interface JsonWrapped { @@ -71,4 +72,11 @@ * wrapping defined in a supertype). */ String value(); + + /** + * Property that is usually only used when overriding (masking) annotations, + * using mix-in annotations. Otherwise default value of {@code true} is fine, + * and value need not be explicitly included. + */ + boolean enabled() default true; } diff --git a/src/test/java/com/fasterxml/jackson/annotation/JsonWrappedTest.java b/src/test/java/com/fasterxml/jackson/annotation/JsonWrappedTest.java index ce6522d5..8d243d3d 100644 --- a/src/test/java/com/fasterxml/jackson/annotation/JsonWrappedTest.java +++ b/src/test/java/com/fasterxml/jackson/annotation/JsonWrappedTest.java @@ -1,61 +1,53 @@ package com.fasterxml.jackson.annotation; +import java.lang.reflect.Constructor; import org.junit.jupiter.api.Test; - import static org.junit.jupiter.api.Assertions.*; public class JsonWrappedTest extends AnnotationTestUtil { - private static class TestClass { - @JsonWrapped("chr") - public String chromosome; - - @JsonWrapped("chr") - public int position; + private static class BeanWithField { + @JsonWrapped("wrapper") + public String field; } - private static class TestClassWithGetter { - private String data; - + private static class BeanWithMethod { @JsonWrapped("wrapper") - public String getData() { - return data; - } + public String getField() { return null; } } - private static class TestClassEmptyWrapper { - @JsonWrapped("") - public String field; + private static class BeanWithParam { + public BeanWithParam(@JsonWrapped("wrapper") String field) { } } + @JsonWrapped("wrapper") + @JacksonAnnotationsInside + @interface BundleAnnotation { } + @Test - public void testAnnotationRetentionAtRuntime() throws Exception { - // Verify annotation is retained at runtime - JsonWrapped ann = TestClass.class.getField("chromosome").getAnnotation(JsonWrapped.class); - assertNotNull(ann, "Annotation should be retained at runtime"); - assertEquals("chr", ann.value()); + public void testRuntimeRetentionOnField() throws Exception { + JsonWrapped ann = BeanWithField.class.getField("field").getAnnotation(JsonWrapped.class); + assertNotNull(ann); + assertTrue(ann.enabled()); } @Test - public void testAnnotationValue() throws Exception { - JsonWrapped ann = TestClass.class.getField("chromosome").getAnnotation(JsonWrapped.class); - assertEquals("chr", ann.value()); - - JsonWrapped ann2 = TestClass.class.getField("position").getAnnotation(JsonWrapped.class); - assertEquals("chr", ann2.value()); + public void testRuntimeRetentionOnMethod() throws Exception { + JsonWrapped ann = BeanWithMethod.class.getMethod("getField").getAnnotation(JsonWrapped.class); + assertNotNull(ann); } @Test - public void testEmptyStringValue() throws Exception { - JsonWrapped ann = TestClassEmptyWrapper.class.getField("field").getAnnotation(JsonWrapped.class); - assertEquals("", ann.value()); + public void testApplicableOnConstructorParameter() throws Exception { + Constructor ctor = BeanWithParam.class.getDeclaredConstructor(String.class); + JsonWrapped ann = ctor.getParameters()[0].getAnnotation(JsonWrapped.class); + assertNotNull(ann); } @Test - public void testAnnotationOnMethod() throws Exception { - JsonWrapped ann = TestClassWithGetter.class.getMethod("getData").getAnnotation(JsonWrapped.class); - assertNotNull(ann, "Annotation should be applicable to methods"); - assertEquals("wrapper", ann.value()); + public void testApplicableOnAnnotationType() { + JsonWrapped ann = BundleAnnotation.class.getAnnotation(JsonWrapped.class); + assertNotNull(ann); } } From 6538d70e7b571e4d9471a049a7d9c5174b902754 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 4 May 2026 10:55:36 -0700 Subject: [PATCH 3/3] Fix issue ref (must refer to in-repo issue or PR) --- release-notes/VERSION-2.x | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 80d00c0c..e7ab53f8 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -14,6 +14,11 @@ NOTE: Jackson 3.x components rely on 2.x annotations; there are no separate === Releases === ------------------------------------------------------------------------ +(not yet released) + +#346: Add `@JsonWrapped` annotation for grouping bean properties into a + nested JSON object (inverse of `@JsonUnwrapped`) + 2.22 (not yet released) #78: Add `@JsonApplyView` to allow changing active JsonView on submodels @@ -23,8 +28,6 @@ NOTE: Jackson 3.x components rely on 2.x annotations; there are no separate #342: Add `@JsonTypeInfo.writeTypeIdForDefaultImpl` to allow skipping writing of type id for values of default type #344: Improve `Locale` handling in `JsonFormat.Value` -#512: Add `@JsonWrapped` annotation for grouping bean properties into a - nested JSON object (inverse of `@JsonUnwrapped`) 2.21 (18-Jan-2026)