Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
74 changes: 74 additions & 0 deletions src/main/java/com/fasterxml/jackson/annotation/JsonWrapped.java
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>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).
*
* <p>Example: given a POJO such as:
* <pre>
* public class Gene {
* public String name;
*
* &#64;JsonWrapped("chr")
* public String chromosome;
*
* &#64;JsonWrapped("chr")
* public int position;
* }
* </pre>
* serialization produces:
* <pre>
* {
* "name" : "BRCA1",
* "chr" : {
* "chromosome" : "17",
* "position" : 43044295
* }
* }
* </pre>
*
* <p>Constraints:
* <ul>
* <li>Non-scalar field types (POJOs, collections, maps, arrays) are supported for
* baseline serialization and deserialization. Each inner field serializes under
* 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.</li>
* <li>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.</li>
* <li>The wrapper name must not conflict with an existing non-wrapped property on the same bean.</li>
* <li>Not supported on {@code @JsonCreator} constructor or factory-method parameters.</li>
* <li>MVP limitation: {@code @JsonView} on inner wrapped fields is ignored — the wrapper
* is always emitted and all inner fields are always included regardless of active view.</li>
* <li>MVP limitation: class-level {@code @JsonFilter} still applies to the wrapper property
* by its wrapper name (the whole wrapper can be suppressed if the filter excludes it),
* but inner fields are not individually filtered.</li>
* <li>MVP limitation: class-level {@code @JsonInclude} (e.g. {@code NON_NULL}) still applies
* to inner wrapped fields during serialization.</li>
* </ul>
*
* @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();
Comment thread
cowtowncoder marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -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());
}
}