DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Parsing JSON in Spring Boot, part 2
scottshipp
scottshipp

Posted on • Updated on • Originally published at code.scottshipp.com

Parsing JSON in Spring Boot, part 2

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"
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Photo by Thomas Kolnowski on Unsplash

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"
}
Enter fullscreen mode Exit fullscreen mode

What if someone sends this?

{
    "description": "Apple iPhone 11",
    "releaseDate": "September 20, 2019",
    "lifecycle": "ACTIVE",
    "carriers": ["T-Mobile", "Verizon", "AT&T"]
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
brunooliveira profile image
Bruno Oliveira

Awesome stuff!!

Collapse
dennishiller profile image
dennishiller

Helped me a lot! Thanks!

Collapse
vangalanikhil profile image
nikhil

need to update the link in the first line.

🌚 Life is too short to browse without dark mode