
Deserializing API Responses Into Java Records
Here is a recipe for deserializing API responses into the new Java construct: Record, and using it for easier verification.
NOTE: At the time of this writing, Record is a preview featuring, meaning it’s not officially a permanent feature of Java yet. It’s been introduced in Java 14 to allow developers to provide feedback on its implementation. So, implementation can change or the feature can be totally removed. I say all of that to say, don’t use this in your production code just yet.
Recipe to Deserialize API Responses Into Java Records
Ingredients
- Rest-Assured (or any other library for making requests: e.g. HTTPClient)
- Jackson binding
- Java 14+ (for Records)
Instructions
- Create Records for each expected object in the response body
- Create instance of Record to serialize expected response
- After making an API request, deserialize response into the Record representing the top-level object in the body
- Compare created instance with deserialized object
Live Stream Video
Ingredients
1 Rest-Assured
My favorite library to make API requests is Rest-Assured. To use it in our code, we need to add it as a dependency. I’m using Maven so I add it to my pom.xml file.
1 2 3 4 5 6 |
<dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <version>4.3.0</version> <scope>test</scope> </dependency> |
2 Jackson Binding
The Jackson binding library assists with deserializing API responses and mapping certain fields (like ones with spaces or a different name) to our Java fields. I add this to the pom.xml as well.
1 2 3 4 5 |
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.11.0</version> </dependency> |
3 Java 14+
Records were released as a preview feature as part of Java 14. So you’ll need at least this version of Java to use the feature. IntelliJ allows us to enable preview features. So, be sure to set this up appropriately.
Instructions
1 Create Records to serialize expected response
The API we’re going to be using is Zippopotamus which given a zip code, it returns information about the location.
Request
1 |
GET http://api.zippopotam.us/us/90210 |
Response
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "post code": "90210", "country": "United States", "country abbreviation": "US", "places": [ { "place name": "Beverly Hills", "longitude": "-118.4065", "state": "California", "state abbreviation": "CA", "latitude": "34.0901" } ] } |
Based on this response, we create a Record to model it.
1 2 3 4 5 6 7 8 9 10 |
package models; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; public record Location( @JsonProperty("post code") String postCode, String country, @JsonProperty("country abbreviation") String countryAbbreviation, List<Places>places) { } |
1 2 3 4 5 6 7 8 9 10 |
package models; import com.fasterxml.jackson.annotation.JsonProperty; public record Places( @JsonProperty("place name") String placeName, String longitude, String state, @JsonProperty("state abbreviation") String stateAbbreviation, String latitude) { } |
Notice with Records, we only need to specify the fields. We do not need to explicitly define getter and setter methods, nor override the inherited equals(), hashCode(), or toString() methods. We get this for free! 🎉 (watch out Lombok)
1 2 3 4 5 6 7 8 9 10 |
package models; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; public record Location( @JsonProperty("post code") String postCode, String country, @JsonProperty("country abbreviation") String countryAbbreviation, List<Places>places) { } |
1 2 3 4 5 6 7 8 9 10 |
package models; import com.fasterxml.jackson.annotation.JsonProperty; public record Places( @JsonProperty("place name") String placeName, String longitude, String state, @JsonProperty("state abbreviation") String stateAbbreviation, String latitude) { } |
2 Create instance of Record representing expected response
In a separate class, I created a test. Within the test method, I created an instance of a Record to represent the response that I’m expecting from my call.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package apis; import models.Location; import models.Places; import org.junit.jupiter.api.Test; import java.util.List; public class ZipCodeTests { @Test public void testBeverlyHills(){ var la = new Location( "90210", "United States", "US", List.of(new Places( "Beverly Hills", "-118.4065", "California", "CA", "34.0901"))); } } |
3 Deserialize response into the Record
Next, I make the API request using Rest-Assured and then call the as() method, which deserializes the response body into a Java object. I pass in the Record which represents the top level object of the response.
23 24 25 |
var expectedLocation = given() .get("http://api.zippopotam.us/us/90210") .as(Location.class); |
By printing out expectedLocation, I can see that the Location record was properly populated!
1 |
Location[postCode=90210, country=United States, countryAbbreviation=US, places=[Places[placeName=Beverly Hills, longitude=-118.4065, state=California, stateAbbreviation=CA, latitude=34.0901]]] |
4 Compare expected result with actual result
And since the equals() method of our Location record is already taken care of by default, asserting against it is a breeze!
29 |
assertEquals(la, expectedLocation); |
Full Test Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
package apis; import models.Location; import models.Places; import org.junit.jupiter.api.Test; import java.util.List; import static io.restassured.RestAssured.given; import static org.junit.jupiter.api.Assertions.assertEquals; public class ZipCodeTests { @Test public void testBeverlyHills(){ var la = new Location( "90210", "United States", "US", List.of(new Places( "Beverly Hills", "-118.4065", "California", "CA", "34.0901"))); var expectedLocation = given() .get("http://api.zippopotam.us/us/90210") .as(Location.class); assertEquals(la, expectedLocation); } } |
Cay Horstmann
Very nice. I’d rename Places as Place since each instance describes a single place.
Angie Jones
In the video, I originally named it
place
and the deserialization didn’t work because it didn’t map toplaces
in the response. Technically, it can work now because I used @JsonProperty anyway but when Jackson supports records, I’ll take those annotations out and it’ll need to match the field name in the response.Tom
Interesting. I’ve always used Lombok for setters/getters etc but never liked it.
Preeti
HI,
Alex
Hi,
kosmik technologies
Hi, Thank you for sharing this informative blog… It’s nice!
Kosmik
Nice Article very glad to read your Article
Thanks & Regards
V.saibabu