When working with JSON in Java, Jackson is a popular choice for serializing and deserializing objects. A common requirement is to produce different JSON representations of the same object, such as a public view with a subset of fields and an internal view with all fields. While you could use multiple Data Transfer Objects (DTOs), this approach often leads to boilerplate code due to field duplication and the need for conversion logic.
Jackson's @JsonView annotation offers a cleaner solution by allowing you to define multiple views for a single Java class. This feature lets you control which properties are included in the JSON output based on the active view.
This example demonstrates how to use @JsonView for serialization and deserialization with different views.
Setting up the Project ¶
We'll start with a small Maven project. The only dependency we need is jackson-databind.
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>3.0.2</version>
</dependency>
Defining Views ¶
First, we need to define the views. Views are typically empty static inner classes within a container class. In this example, we'll create two views: Public and Internal. Internal extends Public, so it includes all fields from the Public view as well as its own.
public class View {
public static class Public {}
public static class Internal extends Public {}
}
Annotating the Model ¶
Next, we annotate the properties of the model class with @JsonView. A field can belong to one or more views. You can annotate either instance variables or getter methods. @JsonView works with both Java classes and records.
public record User(
@JsonView(View.Public.class) String name,
@JsonView(View.Public.class) String email,
@JsonView({View.Public.class}) String username,
@JsonView(View.Internal.class) String ssn,
@JsonView(View.Internal.class) String internalId) {}
- The
name,email, andusernameproperties are part of thePublicview. - The
ssnandinternalIdproperties are part of theInternalview only. BecauseInternalextendsPublic, when you use theInternalview, these properties appear alongside allPublicview properties.
Serialization with @JsonView ¶
Now, let's serialize the object using different views. We can use ObjectMapper.writerWithView() to select the view for serialization.
ObjectMapper mapper = new ObjectMapper();
User user = new User("John Doe", "john@example.com", "johndoe", "123-45-6789", "INT001");
String publicJson = mapper.writerWithView(View.Public.class).writeValueAsString(user);
System.out.println(publicJson);
// Output: {"name":"John Doe","email":"john@example.com","username":"johndoe"}
String internalJson = mapper.writerWithView(View.Internal.class).writeValueAsString(user);
System.out.println(internalJson);
// Output: {"name":"John Doe","email":"john@example.com","username":"johndoe","ssn":"123-45-6789","internalId":"INT001"}
String fullJson = mapper.writeValueAsString(user);
System.out.println(fullJson);
// Output: {"name":"John Doe","email":"john@example.com","username":"johndoe","ssn":"123-45-6789","internalId":"INT001"}
String fullJsonForDeser =
"{\"name\":\"Jane Smith\",\"email\":\"jane@example.com\",\"username\":\"janesmith\",\"ssn\":\"987-65-4321\",\"internalId\":\"INT002\"}";
When the application serializes the User object with the Public view, only the Public view properties appear in the JSON output. When using the Internal view, all properties from both Public and Internal views are included because Internal extends Public. If no view is specified, all properties are serialized.
DEFAULT_VIEW_INCLUSION ¶
When a view is active, Jackson 3, by default, ingores all properties that are not annotated with @JsonView. Note that this is different from Jackson 2, where unannotated properties were always included in all views.
Let's demonstrate this with a simple example. In the Article record below, the title field is annotated with @JsonView, while notes is not.
public record Article(@JsonView(View.Public.class) String title, String notes) {}
When an Article instance is serialized with the Public view, the output will include only title because notes is not annotated with any view.
ObjectMapper defaultNoInclusion = new ObjectMapper();
String withViewDefault =
defaultNoInclusion.writerWithView(View.Public.class).writeValueAsString(article);
System.out.println(withViewDefault);
// Output: {"title":"Hello Views"}
This behavior is controlled by the MapperFeature.DEFAULT_VIEW_INCLUSION feature, which is disabled by default. If you enable it, Jackson includes all unannotated properties in every view.
ObjectMapper withInclusion =
JsonMapper.builder().configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true).build();
String withViewDisabled =
withInclusion.writerWithView(View.Public.class).writeValueAsString(article);
System.out.println(withViewDisabled);
// Output: {"title":"Hello Views","notes":"internal notes"}
Note that DEFAULT_VIEW_INCLUSION only affects serialization when a view is active. If you serialize without a view, it has no effect, and all properties will be included in the output.
String noView = mapper.writeValueAsString(article);
System.out.println(noView);
// Output: {"title":"Hello Views","notes":"internal notes"}
Deserialization with @JsonView ¶
@JsonView is also applicable for deserialization. When you deserialize with a view, only the properties included in that view are populated in the resulting object. All other properties are ignored and set to their default values.
String fullJsonForDeser =
"{\"name\":\"Jane Smith\",\"email\":\"jane@example.com\",\"username\":\"janesmith\",\"ssn\":\"987-65-4321\",\"internalId\":\"INT002\"}";
User publicUser =
mapper.readerWithView(View.Public.class).forType(User.class).readValue(fullJsonForDeser);
System.out.println(publicUser);
// Output: User[name=Jane Smith, email=jane@example.com, username=janesmith, ssn=null, internalId=null]
User internalUser =
mapper.readerWithView(View.Internal.class).forType(User.class).readValue(fullJsonForDeser);
System.out.println(internalUser);
// Output: User[name=Jane Smith, email=jane@example.com, username=janesmith, ssn=987-65-4321, internalId=INT002]
User fullUser = mapper.readValue(fullJsonForDeser, User.class);
System.out.println(fullUser);
// Output: User[name=Jane Smith, email=jane@example.com, username=janesmith, ssn=987-65-4321, internalId=INT002]
In the first example, the application deserializes the JSON string using the Public view. As a result, only the name, email, and username properties are populated in the User object. The ssn and internalId properties, which are not part of the Public view, are set to null.
Deserializing with the Internal view populates all properties since Internal includes both Public properties and its own.
When no view is specified for deserialization, all matching properties from the JSON populate the object.
DEFAULT_VIEW_INCLUSION also affects deserialization. By default, properties without a @JsonView annotation are not deserialized when a view is active. If you enable DEFAULT_VIEW_INCLUSION, unannotated properties will be included during deserialization when a view is active.
String articleJson = "{\"title\":\"Sample Article\",\"notes\":\"secret notes\"}";
Article publicArticleDefault =
mapper.readerWithView(View.Public.class).forType(Article.class).readValue(articleJson);
System.out.println(publicArticleDefault);
// Output: Article[title=Sample Article, notes=null]
withInclusion =
JsonMapper.builder().configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true).build();
Article publicArticleNoDefault =
withInclusion
.readerWithView(View.Public.class)
.forType(Article.class)
.readValue(articleJson);
System.out.println(publicArticleNoDefault);
// Output: Article[title=Sample Article, notes=secret notes]
Article fullArticle = mapper.readValue(articleJson, Article.class);
System.out.println(fullArticle);
// Output: Article[title=Sample Article, notes=secret notes]
When not not using a view, the flag DEFAULT_VIEW_INCLUSION has no effect, and all properties are populated.
Conclusion ¶
Jackson's @JsonView is a powerful tool for controlling serialization and deserialization, allowing you to define multiple views on the same model. This is particularly useful for exposing different representations of an object. Instead of creating multiple DTOs, you can use @JsonView to annotate the properties in a single Java class or record, which helps reduce boilerplate and keeps your codebase cleaner.
However, it is important to be careful with this feature and always use a reader (readerWithView()) or writer (writerWithView()) with a view. If you forget to do so, all properties will be serialized or deserialized, which could lead to data leaks.