Jackson JSON Request and Response Mapping in Spring Boot Jackson JSON Request and Response Mapping in Spring Boot

Page content

In this tutorial, we’ll learn how to map Jackson JSON request and response in Spring Boot Application with various Jackson configurations.

Jackson JSON ObjectMapper

When you create a @RestController in a Spring Boot application to define API endpoints then Jackson JSON ObjectMapper is the default HTTP Converter of your web application which does two things:

  1. Map the Java Object to JSON Response when you return the Object from GET request method like this:-

    @GetMapping
    public List<User> getAllUsers()
    

    Converting the Java Object to JSON is known as Marshalling or Serialization

  2. Map the JSON to Java Object when you add a @RequestBody argument in POST request method like this:-

    @PostMapping
    public Long createUser(@RequestBody User user)
    

    Converting the JSON to Java Object is known as Unmarshalling or Deserialization

We will look at various configurations of serialization and deserialization using Jackson ObjectMapper with examples that can help to resolve common issues related to JSON mapping in API development.

API Development

Let’s quickly develop some GET and POST APIs using the Controller class UserController, we will use this to demonstrate various Jackson configurations and their impact on JSON.

@RestController
@RequestMapping("/users")
public class UserController {

	@Autowired
	private UserService userService;

	@GetMapping
	public List<User> getAllUsers() {
		return userService.getAllUsers();
	}

	@GetMapping("/{id}")
	public User getUserById(@PathVariable Long id) {
		return userService.getUserById(id);
	}

	@PostMapping
	@ResponseStatus(HttpStatus.CREATED)
	public Long createUser(@RequestBody User user) {
		return userService.createUser(user);
	}
}

public class User {
  private Long id;
  private String name;
  private LocalDate dateOfBirth;
  /* Getters and Setters */	
}

Let’s look at various important configurations and their impact on API requests and responses.

Prevent Failure on Unknown Property in JSON Request Body

If there are unknown properties in JSON Request Body that cannot be mapped to User Java Object then Jackson ObjectMapper throws UnrecognizedPropertyException. This is a default behavior.

spring.jackson.deserialization.FAIL_ON_UNKNOWN_PROPERTIES = true (default)

Let’s add additional field gender in POST request body which is missing in User request mapping java Object. It will throw Json parse error Unrecognized field "gender" as in below example:-

application.yml
spring.jackson.deserialization.FAIL_ON_UNKNOWN_PROPERTIES: true
Request
curl -X POST \ http://localhost:8080/users \ -H 'cache-control: no-cache' \ -H 'content-type: application/json' \ -d '{ "id": 1, "name": "Ashish", "dateOfBirth": "1986-08-22", "gender": "male" }'
Response (Error)
JSON parse error: Unrecognized field "gender" (class com.example.api.model.User), not marked as ignorable; nested exception is com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "gender" (class com.example.api.model.User), not marked as ignorable (3 known properties: "dateOfBirth", "id", "name"])

We can prevent this failure by setting the FAIL_ON_UNKNOWN_PROPERTIES property to false and allowing unknown properties (or additional fields) in our JSON Request Body. If you are using Spring Boot default ObjectMapper then this is the default behavior.

spring.jackson.deserialization.FAIL_ON_UNKNOWN_PROPERTIES = false

We see that POST request did not fail this time and returned id of created user.

application.yml
spring.jackson.deserialization.FAIL_ON_UNKNOWN_PROPERTIES: false
Request
curl -X POST \ http://localhost:8080/users \ -H 'cache-control: no-cache' \ -H 'content-type: application/json' \ -d '{ "id": 1, "name": "Ashish", "dateOfBirth": "1986-08-22", "gender": "male" }'
Response
1

You can also programmatically deserialize a JSON to Java Object with unknown properties using Jackson ObjectMapper like this:-

ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
User user = mapper.readValue(jsonAsString, User.class);

Don’t allow certain property in JSON Request Body

Sometimes we don’t want certain properties such as id to be passed in the request body because we would be auto-generating that id in the backend. In such case, you can annotate such properties with @JsonIgnore.

@JsonIgnore Annotation
package com.example.api.model;

public class User {

  @JsonIgnore
  private Long id;
	
  private String name;
	
  private LocalDate dateOfBirth;
  
	/* Getters and Setters */	
}

spring.jackson.deserialization.FAIL_ON_IGNORED_PROPERTIES = false (default)

By default, Jackson ObjectMapper allows all the properties to be passed in the request body even those annotated with @JsonIgnore. We see that it allows id to be passed in the request body:-

application.yml
spring.jackson.deserialization.FAIL_ON_IGNORED_PROPERTIES: false
Request
curl -X POST \ http://localhost:8080/users \ -H 'cache-control: no-cache' \ -H 'content-type: application/json' \ -d '{ "id": 1, "name": "Ashish", "dateOfBirth": "1986-08-22" }'
Response
1

spring.jackson.deserialization.FAIL_ON_IGNORED_PROPERTIES = true

Setting FAIL_ON_IGNORED_PROPERTIES property to true will throw IgnoredPropertyException when id property which is annotated with @JsonIgnore is passed in request body:-

application.yml
spring.jackson.deserialization.FAIL_ON_IGNORED_PROPERTIES: true
Request
curl -X POST \ http://localhost:8080/users \ -H 'cache-control: no-cache' \ -H 'content-type: application/json' \ -d '{ "id": 1, "name": "Ashish", "dateOfBirth": "1986-08-22" }'
Response (Error)
JSON parse error: Ignored field "id" (class com.example.api.model.User) encountered; mapper configured not to allow this; nested exception is com.fasterxml.jackson.databind.exc.IgnoredPropertyException: Ignored field "id" (class com.example.api.model.User) encountered; mapper configured not to allow this (3 known properties: "dateOfBirth", "name", "lastLogin"])

Don’t include properties with null value in JSON Response

We generally do not want to include the properties with NULL values in JSON response. It does make sense to exclude them to reduce the network load.

spring.jackson.default-property-inclusion: use_defaults (default)

Jackson includes the null properties in the JSON response by default.

application.yml
spring.jackson.default-property-inclusion: use_defaults
Request
curl -X GET http://localhost:8080/users
Response
[{"id":1,"name":"Adam","dateOfBirth":null}, {"id":2,"name":"Bob","dateOfBirth":null}, {"id":3,"name":"Charlie","dateOfBirth":null}]

spring.jackson.default-property-inclusion: non_null

We can set the application property spring.jackson.default-property-inclusion to non_null to not include null properties.

application.yml
spring.jackson.default-property-inclusion: non_null
Request
curl -X GET http://localhost:8080/users
Response
[{"id":1,"name":"Adam"}, {"id":2,"name":"Bob"}, {"id":3,"name":"Charlie"}]

@JsonInclude Annotation

For older versions of Spring boot, where the property spring.jackson.default-property-inclusion doesn’t work, you can use @JsonInclude(Include.NON_NULL) annotation at the class or field level.

package com.example.api.model;

public class User {

  private Long id;
	
  private String name;
	
  @JsonInclude(JsonInclude.Include.NON_NULL)
  private LocalDate dateOfBirth;
  
	/* Getters and Setters */	
}

Please note that field-level annotation overrides the class-level annotation, and class-level annotation overrides the application-level property.

Pretty Print JSON Response

Pretty print or formatted JSON response in the development environment makes it easier to read and debug API responses.

spring.jackson.serialization.INDENT_OUTPUT = false (default)

API JSON responses are not pretty print (formatted) by default.

application.yml
spring.jackson.serialization.INDENT_OUTPUT: false
Request
curl -X GET http://localhost:8080/users
Response
[{"id":1,"name":"Adam","dateOfBirth":"1950-01-01"},{"id":2,"name":"Bob","dateOfBirth":"1990-10-30"},{"id":3,"name":"Charlie","dateOfBirth":"1979-07-26"}]

spring.jackson.serialization.INDENT_OUTPUT = true

We can pretty print JSON Response by setting INDENT_OUTPUT property to true . We see that JSON response is formatted after that.

application.yml
spring.jackson.serialization.INDENT_OUTPUT: true
Request
curl -X GET http://localhost:8080/users
Response (Pretty Print)
[ { "id" : 1, "name" : "Adam", "dateOfBirth" : "1950-01-01" }, { "id" : 2, "name" : "Bob", "dateOfBirth" : "1990-10-30" }, { "id" : 3, "name" : "Charlie", "dateOfBirth" : "1979-07-26" } ]

Format Date and DateTime properties in JSON Response

Java Date and Time Objects such as Date, LocalDate, LocalDateTime, ZonedDateTime are converted into numeric timestamps by default during JSON serialization.

spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS = true (default)
application.yml
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS: true
Request
curl -X GET http://localhost:8080/users/1
Response
{ "id" : 1, "name" : "Adam", "dateOfBirth" : [ 1950, 1, 1 ], "lastLogin" : [ 2020, 7, 3, 0, 26, 22, 211000000 ], "zonedDateTime": 1622874073.231148 }

We can disable this behavior to allow Jackson to convert Date and Time types of objects into human-readable String format. Please note that if you are using Spring Boot’s default ObjectMapper then WRITE_DATES_AS_TIMESTAMPS property is set to false by default.


spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS = false
application.yml
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS: false
Request
curl -X GET http://localhost:8080/users/1
Response
{ "id" : 1, "name" : "Adam", "dateOfBirth" : "1950-01-01", "lastLogin" : "2020-07-03T00:28:32.394" "zonedDateTime": "2021-06-05T14:22:45.066295+08:00" }

We see that date and time fields are in human-readable format in JSON response after disabling this feature.


@JsonFormat Annotation

We can further customize the Date, LocalDate, LocalDateTime, ZonedDateTime Object types by annotating them with @JsonFormat annotations in our User Object Model.

package com.example.api.model;

public class User {
	
	private Long id;
	
	private String name;
	
	@JsonFormat(pattern="dd MMM yyyy")
	private LocalDate dateOfBirth;
	
	@JsonFormat(pattern="dd MMM yyyy hh:mm:ss")
	private LocalDateTime lastLogin;
	
	@JsonFormat(pattern = "yyyy-MM-dd@HH:mm:ss.SSSXXX", locale = "en_SG", timezone = "Asia/Singapore")
	private ZonedDateTime zonedDateTime;

	/* Getters and Setters */	
}

Output will be something like this after applying @JsonFormat annotations:

application.yml
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS: false
Request
curl -X GET http://localhost:8080/users/1
Response
{ "id" : 1, "name" : "Adam", "dateOfBirth" : "01 Jan 1950", "lastLogin" : "03 Jul 2020 01:03:34", "zonedDateTime" : "2020-07-03@01:03:34.467+08:00" }

Please note that once you apply @JsonFormat annotation on Date and Time Object types, it is used in both serialization (object to JSON) and deserialization (JSON to object). That means you need to pass the date or time parameters in the same format in HTTP request body JSON if required.


@JsonSerialize Annotation

When you use @JsonFormat on LocalDate, LocalDateTime and ZonedDateTime then sometimes Jackson throw InvalidDefinitionException during serialization like this:-

Response (Error)
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDate` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: com.example.api.domain.User["dateOfBirth"])

You can prevent this issue by adding com.fasterxml.jackson.datatype:jackson-datatype-jsr310 dependency and using @JsonSerialize annotation on date and time field types as below:-

public class User {

	Long id;

	String name;
	
	@JsonFormat(pattern="dd MMM yyyy")
	@JsonSerialize(using = LocalDateSerializer.class)
	LocalDate dateOfBirth;

	@JsonFormat(pattern="dd MMM yyyy hh:mm:ss")
	@JsonSerialize(using = LocalDateTimeSerializer.class)
	LocalDateTime lastLogin;
	
	@JsonFormat(pattern = "yyyy-MM-dd@HH:mm:ss.SSSXXX", locale = "en_SG", timezone = "Asia/Singapore")
	@JsonSerialize(using = ZonedDateTimeSerializer.class)
	ZonedDateTime zonedDateTime;
}

Conclusion

We looked at some of the useful configurations. Here is the full list of Jackson serialization and deserialization properties configurable in Spring Boot Application using application.yml, or application.properties file.

application.yml
spring:
  jackson:
    default-property-inclusion: use_defaults/always/non-null/non-empty/non-absent/non-default
    serialization:
      CLOSE_CLOSEABLE: true/false
      EAGER_SERIALIZER_FETCH: true/false
      FAIL_ON_EMPTY_BEANS: true/false
      FAIL_ON_SELF_REFERENCES: true/false
      FAIL_ON_UNWRAPPED_TYPE_IDENTIFIERS: true/false
      FLUSH_AFTER_WRITE_VALUE: true/false
      INDENT_OUTPUT: true/false
      ORDER_MAP_ENTRIES_BY_KEYS: true/false
      USE_EQUALITY_FOR_OBJECT_ID: true/false
      WRAP_EXCEPTIONS: true/false
      WRAP_ROOT_VALUE: true/false
      WRITE_BIGDECIMAL_AS_PLAIN: true/false
      WRITE_CHAR_ARRAYS_AS_JSON_ARRAYS: true/false
      WRITE_DATES_AS_TIMESTAMPS: true/false
      WRITE_DATES_WITH_ZONE_ID: true/false
      WRITE_DATE_KEYS_AS_TIMESTAMPS: true/false
      WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS: true/false
      WRITE_DURATIONS_AS_TIMESTAMPS: true/false
      WRITE_EMPTY_JSON_ARRAYS: true/false
      WRITE_ENUMS_USING_INDEX: true/false
      WRITE_ENUMS_USING_TO_STRING: true/false
      WRITE_ENUM_KEYS_USING_INDEX: true/false
      WRITE_NULL_MAP_VALUES: true/false
      WRITE_SELF_REFERENCES_AS_NULL: true/false
      WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED: true/false
    deserialization:
      ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT: true/false
      ACCEPT_EMPTY_STRING_AS_NULL_OBJECT: true/false
      ACCEPT_FLOAT_AS_INT: true/false
      ACCEPT_SINGLE_VALUE_AS_ARRAY: true/false
      ADJUST_DATES_TO_CONTEXT_TIME_ZONE: true/false
      EAGER_DESERIALIZER_FETCH: true/false
      FAIL_ON_IGNORED_PROPERTIES: true/false
      FAIL_ON_INVALID_SUBTYPE: true/false
      FAIL_ON_MISSING_CREATOR_PROPERTIES: true/false
      FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY: true/false
      FAIL_ON_NULL_CREATOR_PROPERTIES: true/false
      FAIL_ON_NULL_FOR_PRIMITIVES: true/false
      FAIL_ON_NUMBERS_FOR_ENUMS: true/false
      FAIL_ON_READING_DUP_TREE_KEY: true/false
      FAIL_ON_TRAILING_TOKENS: true/false
      FAIL_ON_UNKNOWN_PROPERTIES: true/false
      FAIL_ON_UNRESOLVED_OBJECT_IDS: true/false
      READ_DATE_TIMESTAMPS_AS_NANOSECONDS: true/false
      READ_ENUMS_USING_TO_STRING: true/false
      READ_UNKNOWN_ENUM_VALUES_AS_NULL:true/false
      READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE: true/false
      UNWRAP_ROOT_VALUE: true/false
      UNWRAP_SINGLE_VALUE_ARRAYS: true/false
      USE_BIG_DECIMAL_FOR_FLOATS: true/false
      USE_BIG_INTEGER_FOR_INTS: true/false
      USE_JAVA_ARRAY_FOR_JSON_ARRAY: true/false
      USE_LONG_FOR_INTS: true/false
      WRAP_EXCEPTIONS: true/false

You can refer to the Spring Boot official documentation for the customization of Jackson ObjectMapper.

Please note that spring boot configuration supports Relaxed Binding which means properties can be in uppercase or lowercase, both are valid.

spring.jackson.serialization.INDENT_OUTPUT: true

is same as

spring.jackson.serialization.indent_output: true

That’s it for now. I’ll keep updating this post with more practical use cases as I come across them.

Download the source code for these examples from github/springboot-api