In Part 1, we covered some simple parsing examples:
A simple JSON request body with two string members which Spring Boot / Jackson parsed as a mutable Java object
The same simple JSON request body parsed as an immutable Java object
A more complex JSON request body that had one string member and one object member
The more complex object mentioned in the prior bullet point with the addition of an array of values
A JSON request body which represented an array of objects
A “wrapped” JSON object
In this next part, we’re going to cover four new use cases you are likely to encounter at some point:
Handling dates
Handling enumerations
Ignoring fields we don’t care about
Sensibly defaulting null values
Handling Dates
Continuing on with the example from Part 1, let’s consider if our JSON representation of a smartphone had just a description (like “Samsung Galaxy S20” or “Apple iPhone 11”) and a release date. The Samsung Galaxy S20 might look like this:
{
"description": "Apple iPhone 11",
"releaseDate": "September 20, 2019"
}
The standard Java class for a date is LocalDate and Jackson, the JSON parsing/generating library that Spring Boot uses, can parse JSON to a LocalDate. So the POJO for the above JSON would look like this:
class Smartphone {
private final String description;
private final LocalDate releaseDate;
@JsonCreator
public Smartphone(String description, @JsonFormat(pattern = "MMMM d, yyyy") LocalDate releaseDate) {
this.description = description;
this.releaseDate = releaseDate;
}
public String getDescription() {
return description;
}
public LocalDate getReleaseDate() {
return releaseDate;
}
}
Notice that the only annotation we use here is “@JsonFormat” where we supply a “pattern.” This pattern must be a Java SimpleDateFormat pattern string. Without the @JsonFormat
annotation and the pattern string, you would have to submit only request bodies matching the default format of “yyyy-MM-d” (such as “2019-09-20”).
In my opinion, when working with a LocalDate type in your POJO, you should always use the @JsonFormat annotation regardless of whether you accept the default or not, just to be explicit to future maintainers of the code about what pattern is expected.
With that POJO, the following SmartphoneController.java can be used:
class Smartphone {
private final String description;
private final LocalDate releaseDate;
@JsonCreator
public Smartphone(String description, @JsonFormat(pattern = "MMMM d, yyyy") LocalDate releaseDate) {
this.description = description;
this.releaseDate = releaseDate;
}
public String getDescription() {
return description;
}
public LocalDate getReleaseDate() {
return releaseDate;
}
}
Rebuild and restart the application and test this. You should be able to successfully post the request given above, and you will see logging like:
The Apple iPhone 11 was released on 2019-09-20
You will notice that since the log statement did not format the date but called the default toString() method (intrinsically) that the date was logged in the aforementioned default format.
Now attempt to post a request with a date that does not match this format, such as:
{
"description": "Apple iPhone 11",
"releaseDate": "2019-09-20"
}
You will receive a response like:
{
"timestamp": "2020-04-01T22:05:30.811+0000",
"status": 400,
"error": "Bad Request",
"message": "JSON parse error: Cannot deserialize value of type `java.time.LocalDate` from String \"2019-09-20\": Failed to deserialize java.time.LocalDate: (java.time.format.DateTimeParseException) Text '2019-09-20' could not be parsed at index 0; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.time.LocalDate` from String \"2019-09-20\": Failed to deserialize java.time.LocalDate: (java.time.format.DateTimeParseException) Text '2019-09-20' could not be parsed at index 0\n at [Source: (PushbackInputStream); line: 3, column: 17] (through reference chain: com.scottshipp.code.restservice.datehandling.Smartphone[\"releaseDate\"])",
"path": "/smartphone"
}
The application therefore enforces the date format when the request is made, and responds with a 400 Bad Request if the date format does not match the pattern.
Handling Enumerations
If you request that clients of your API post data from a specific set of constants, then a Java enum is typically used. For example, consider if each phone was marked with a lifecycle status, which is one of RELEASE_ANNOUNCED, RELEASE_DELAYED, ACTIVE, or RETIRED.
JSON for this might look like the following.
{
"description": "Apple iPhone 11",
"releaseDate": "September 20, 2019",
"lifecycle": "ACTIVE"
}
It’s certainly possible to treat lifecycle as a String, but since you know that the value should be one of a fixed set of constants, you can pick up a lot of benefits by making this an enum. Here is a version of Smartphone.java with lifecycle as an enum:
class Smartphone {
private final String description;
private final LocalDate releaseDate;
private final Lifecycle lifecycle;
@JsonCreator
public Smartphone(String description,
@JsonFormat(pattern = "MMMM d, yyyy") LocalDate releaseDate,
Lifecycle lifecycle) {
this.description = description;
this.releaseDate = releaseDate;
this.lifecycle = lifecycle;
}
public String getDescription() {
return description;
}
public LocalDate getReleaseDate() {
return releaseDate;
}
public Lifecycle getLifecycle() {
return lifecycle;
}
enum Lifecycle {
RELEASE_ANNOUNCED, RELEASE_DELAYED, ACTIVE, RETIRED;
}
}
With that in place, you shouldn’t need any changes to SmartphoneController.java. Rebuild and restart the application and give it a shot.
You will also notice that if you attempt to post a value for lifecycle that is not valid, you will get a 400 Bad Request response.
{
"timestamp": "2020-04-01T22:24:38.026+0000",
"status": 400,
"error": "Bad Request",
"message": "JSON parse error: Cannot deserialize value of type `com.scottshipp.code.restservice.datehandling.Smartphone$Lifecycle` from String \"DEPRECATED\": not one of the values accepted for Enum class: [RETIRED, RELEASE_DELAYED, RELEASE_ANNOUNCED, ACTIVE]; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `com.scottshipp.code.restservice.datehandling.Smartphone$Lifecycle` from String \"DEPRECATED\": not one of the values accepted for Enum class: [RETIRED, RELEASE_DELAYED, RELEASE_ANNOUNCED, ACTIVE]\n at [Source: (PushbackInputStream); line: 4, column: 15] (through reference chain: com.scottshipp.code.restservice.datehandling.Smartphone[\"lifecycle\"])",
"path": "/smartphone"
}
Ignoring fields the application doesn’t care about
Consider the idea that a client application adds a new field without the application knowing (or caring) about it. Taking the code we have from the prior example, we can receive a JSON request body like this right now:
{
"description": "Apple iPhone 11",
"releaseDate": "September 20, 2019",
"lifecycle": "DEPRECATED"
}
What if someone sends this?
{
"description": "Apple iPhone 11",
"releaseDate": "September 20, 2019",
"lifecycle": "ACTIVE",
"carriers": ["T-Mobile", "Verizon", "AT&T"]
}
Give it a try and see what happens. In current versions of Spring Boot, this will work just fine. Spring Boot configures Jackson to ignore fields the application doesn’t know about automatically.
Custom null value handling
In a similar manner, null is completely acceptable. For example, again using the existing code, imagine if someone does not supply an expected value like releaseDate, and sends this:
{
"description": "Apple iPhone 11",
"lifecycle": "ACTIVE"
}
What happens in that case is that the application just chooses the value “null” for the releaseDate member. You can see this in the application log which will show:
The Apple iPhone 11 was released on null
This may or may not be desirable. It makes sense if you want to always receive the request that was sent to you the way it was sent. In a similar manner, if you expect a primitive value like a boolean, int, or long, and it is not supplied, the default value will be chosen instead (e.g. an int will default to 0, boolean to false and so forth).
But if you want to change that, you can. Simply add the @JsonProperty
annotation to the field with the required = true
flag.
class Smartphone {
private final String description;
private final LocalDate releaseDate;
private final Lifecycle lifecycle;
@JsonCreator
public Smartphone(String description,
@JsonProperty(required = true)
@JsonFormat(pattern = "MMMM d, yyyy") LocalDate releaseDate,
Lifecycle lifecycle) {
this.description = description;
this.releaseDate = releaseDate;
this.lifecycle = lifecycle;
}
public String getDescription() {
return description;
}
public LocalDate getReleaseDate() {
return releaseDate;
}
public Lifecycle getLifecycle() {
return lifecycle;
}
enum Lifecycle {
RELEASE_ANNOUNCED, RELEASE_DELAYED, ACTIVE, RETIRED;
}
}
Now whenever you send a request without the releaseDate, it will receive a 400 Bad Request response.
Conclusion
As I mentioned in Part 1, I hope this walkthrough of common Spring Boot JSON parsing use cases was useful. Did I miss any? Was anything unclear? Any other comments or questions? Let me know in the comment section below.
Top comments (3)
Awesome stuff!!
Helped me a lot! Thanks!
need to update the link in the first line.