Over the last week, I have encountered a number of questions and issues for which no easy answer could be found via Internet searches. Starting a blog has been on my mind for a while, so I decided today to take the plunge and write about my questions and problems, and the answers I find.
I use
jsonschema2pojo, a nice library that takes JSON schema and generates Plain Old Java Objects (POJOs). The POJOs are used in my Java API as domain model objects. It's useful to generate this code via the JSON schema so future changes are made in one place (the JSON schema) versus two places (the JSON schema and the Java domain model objects). I build my project with maven, so I use the jsonschema2pojo maven plugin to generate the POJOs.
Here is the issue I was having with my JSON: I wanted to include (or serialize) a field both when it had a value and when it was null. So let's look at this problem with a JSON schema example of a playlist and a song.
Let's define a playlist as follows:
{
"$schema": "http://json-schema.org/draft-04/hyper-schema#",
"title": "playList",
"type": "object",
"description": "A list of songs",
"javaType": "org.anna.testJsonSchema.models.v1.PlayList",
"additionalProperties": false,
"required": [
"name",
"songs" ],
"properties": {
"id": {
"description": "Server generated identifier for this playlist",
"type": "string" },
"name": {
"description": "The name of the playlist",
"type": "string" },
"songs": {
"description": "Songs in the playlist",
"type": "array",
"items": {
"$ref": "song.json" },
"minItems": 1,
"maxItems": 100,
"uniqueItems": true }
}
}
And let's define a song like this:
{
"$schema": "http://json-schema.org/draft-04/hyper-schema#",
"title": "document",
"type": "object",
"description": "A song",
"javaType": "org.anna.testJsonSchema.models.v1.Song",
"additionalProperties": false,
"required": [
"artist",
"title" ],
"properties": {
"id": {
"description": "Server generated id",
"type": "string" },
"artist": {
"description": "The song artist",
"type": "string" },
"title": {
"description": "The song title",
"type": "string" },
"genre": {
"description": "The song genre",
"type": "string" }
}
}
In this example, a genre is not required, and let's say that we want the serialized object to either print the genre string or null when none is defined. (As an aside, a genre could be a nice place to use an enumerated type).
Now, the first time I generated the Java code for a Song, this is the resulting class (package, import, and comment statements removed - see what's checked into the github
project):
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({
"id",
"artist",
"title",
"genre"})public class Song {
@JsonProperty("id")
@JsonPropertyDescription("Server generated id")
private String id;
@JsonProperty("artist")
@JsonPropertyDescription("The song artist")
@NotNull private String artist;
@JsonProperty("title")
@JsonPropertyDescription("The song title")
@NotNull private String title;
@JsonProperty("genre")
@JsonPropertyDescription("The song genre")
private String genre;
@JsonProperty("id")
public String getId() {
return id;
}
@JsonProperty("id")
public void setId(String id) {
this.id = id;
}
public Song withId(String id) {
this.id = id;
return this;
}
@JsonProperty("artist")
public String getArtist() {
return artist;
} @JsonProperty("artist")
public void setArtist(String artist) {
this.artist = artist;
}
public Song withArtist(String artist) {
this.artist = artist;
return this;
}
@JsonProperty("title")
public String getTitle() {
return title;
}
@JsonProperty("title")
public void setTitle(String title) {
this.title = title;
}
public Song withTitle(String title) {
this.title = title;
return this;
}
@JsonProperty("genre")
public String getGenre() {
return genre;
}
@JsonProperty("genre")
public void setGenre(String genre) {
this.genre = genre;
}
public Song withGenre(String genre) {
this.genre = genre;
return this;
}
@Override public String toString() {
return ToStringBuilder.reflectionToString(this);
}
@Override public int hashCode() {
return new HashCodeBuilder().append(id)
.append(artist).append(title)
.append(genre).toHashCode();
}
@Override public boolean equals(Object other) {
if (other == this) {
return true;
}
if ((other instanceof Song) == false) {
return false;
}
Song rhs = ((Song) other);
return new EqualsBuilder().append(id, rhs.id)
.append(artist, rhs.artist)
.append(title, rhs.title)
.append(genre, rhs.genre).isEquals();
}
}
I highlighted in yellow the line to look at carefully. That line means that no null values will be included in the JSON. Exactly what I don't want. How to change this?
I tried modifying the JSON schema a few different ways, none of which worked. First I tried making the type either string or null like this:
"genre": {
"description": "The song genre",
"type": ["string", "null"]
}
Next I tried using oneOf for the type like this, which generates an Object for genre in the Java code:
"genre": {
"description": "The song genre",
"oneOf": [
{"type": "null"},
{"type": "string"}
]}
Or even saying the field is nullable:
"genre": {
"description": "The song genre",
"type": "string",
"nullable": true}
The problem is the Song class still has this annotation:
@JsonInclude(JsonInclude.Include.NON_NULL)
I finally discovered a configuration option for jsonschema2pojo that changes that JsonInclude:
inclusionLevel!
So, the build section in my pom.xml now looks like this (I highlighted in yellow the inclusionLevel configuration option):
<build>
<plugins>
<plugin>
<groupId>org.jsonschema2pojo</groupId>
<artifactId>jsonschema2pojo-maven-plugin</artifactId>
<version>0.4.30</version>
<configuration>
<sourceDirectory>${basedir}/src/main/resources/schemas/json/v1</sourceDirectory>
<outputDirectory>${basedir}/src/main/java</outputDirectory>
<targetPackage>org.anna.testJsonSchema.models.v1</targetPackage>
<generateBuilders>true</generateBuilders>
<includeJsr303Annotations>true</includeJsr303Annotations>
<includeConstructors>false</includeConstructors>
<includeAdditionalProperties>true</includeAdditionalProperties>
<initializeCollections>true</initializeCollections>
<targetVersion>42</targetVersion>
<useCommonsLang3>true</useCommonsLang3>
<inclusionLevel>ALWAYS</inclusionLevel>
</configuration>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Regenerating the Java objects, the Song class is now annotated with:
@JsonInclude(JsonInclude.Include.ALWAYS)
And we get null in the JSON! My work is done!
I'll be honest - this solution is not ideal if you don't want all fields in your class with null values to be included in the JSON. I don't know how to solve that issue. Maybe you do? Feel free to comment with ideas.
You can find this Java project in
github.