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>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.20.0</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
, andusername
fields are part of thePublic
view. - The
ssn
andinternalId
fields are part of theInternal
view only. These two fields are also part of thePublic
view becauseInternal
extendsPublic
.
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"}
As you can see, when the application serializes the User
object with the Public
view, only the fields annotated with @JsonView(View.Public.class)
are included in the JSON output. When using the Internal
view, all fields 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, by default, includes properties that are not annotated with @JsonView
. This can be problematic if you forget to annotate a new property. For instance, if a new address
field were added to the User
record without a @JsonView
annotation, it would be included in the JSON output for all views, which may not be the desired behavior.
Let's demonstrate this with a simple example. In the Article
record below, only 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 both title
and notes
by default.
Article article = new Article("Hello Views", "internal notes");
ObjectMapper defaultInclusion = new ObjectMapper();
String withViewDefault =
defaultInclusion.writerWithView(View.Public.class).writeValueAsString(article);
System.out.println(withViewDefault);
// Output: {"title":"Hello Views","notes":"internal notes"}
This behavior is controlled by the MapperFeature.DEFAULT_VIEW_INCLUSION
feature, which is enabled by default. If you disable it, only properties explicitly annotated for the active view will be included.
ObjectMapper noDefaultInclusion =
JsonMapper.builder().configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false).build();
String withViewDisabled =
noDefaultInclusion.writerWithView(View.Public.class).writeValueAsString(article);
System.out.println(withViewDisabled);
// Output: {"title":"Hello Views"}
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 = noDefaultInclusion.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).readValue(fullJsonForDeser, User.class);
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).readValue(fullJsonForDeser, User.class);
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
fields are populated in the User
object. The ssn
and internalId
fields, which are not part of the Public
view, are set to null
.
On the other hand, deserializing with the Internal
view populates all fields, since Internal
includes all properties from Public
as well as its own.
When no view is specified for deserialization, all matching properties from the JSON are populated in the object.
DEFAULT_VIEW_INCLUSION
also affects deserialization. By default, properties without a @JsonView
annotation are included in all views, meaning that if you deserialize with a view, these unannotated properties will still be populated. If you disable DEFAULT_VIEW_INCLUSION
, unannotated properties will be ignored during deserialization when a view is active.
String articleJson = "{\"title\":\"Sample Article\",\"notes\":\"secret notes\"}";
Article publicArticleDefault =
mapper.readerWithView(View.Public.class).readValue(articleJson, Article.class);
System.out.println(publicArticleDefault);
// Output: Article[title=Sample Article, notes=secret notes]
noDefaultInclusion =
JsonMapper.builder().configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false).build();
Article publicArticleNoDefault =
noDefaultInclusion.readerWithView(View.Public.class).readValue(articleJson, Article.class);
System.out.println(publicArticleNoDefault);
// Output: Article[title=Sample Article, notes=null]
Article fullArticle = mapper.readValue(articleJson, Article.class);
System.out.println(fullArticle);
// Output: Article[title=Sample Article, notes=secret notes]
As seen in the previous example, when 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 fields 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. Additionally, consider disabling DEFAULT_VIEW_INCLUSION
to avoid accidentally including unannotated properties in your views.