diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 5464a3af..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 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..ed111abc --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonWrapped.java @@ -0,0 +1,82 @@ +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.ANNOTATION_TYPE, 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(); + + /** + * 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 new file mode 100644 index 00000000..8d243d3d --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/annotation/JsonWrappedTest.java @@ -0,0 +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 BeanWithField { + @JsonWrapped("wrapper") + public String field; + } + + private static class BeanWithMethod { + @JsonWrapped("wrapper") + public String getField() { return null; } + } + + private static class BeanWithParam { + public BeanWithParam(@JsonWrapped("wrapper") String field) { } + } + + @JsonWrapped("wrapper") + @JacksonAnnotationsInside + @interface BundleAnnotation { } + + @Test + public void testRuntimeRetentionOnField() throws Exception { + JsonWrapped ann = BeanWithField.class.getField("field").getAnnotation(JsonWrapped.class); + assertNotNull(ann); + assertTrue(ann.enabled()); + } + + @Test + public void testRuntimeRetentionOnMethod() throws Exception { + JsonWrapped ann = BeanWithMethod.class.getMethod("getField").getAnnotation(JsonWrapped.class); + assertNotNull(ann); + } + + @Test + 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 testApplicableOnAnnotationType() { + JsonWrapped ann = BundleAnnotation.class.getAnnotation(JsonWrapped.class); + assertNotNull(ann); + } +}