Jackson, JSON and the Proper Handling of Unknown Fields in APIs
How to proper deserialize json to java objects without facing UnrecognizedPropertyException when getting unkown fields or properties when consuming re
Imagine the following scenario: You have an application that integrates with another through the consumption of REST endpoints. To perform serialization/deserialization you use the famous Jackson library that magically transforms java objects into JSON (serialization) and vice versa (deserialization). One fine day, quite suddenly, your requests stop working with an exception similar to the one below:
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field
What could have happened? The exception itself tells you: There are unknown attributes in JSON that deserialization is being performed.
Explaining the Exception
According to the official documentation:
Specialized JsonMappingException sub-class specifically used to indicate problems due to encountering a JSON property that could not be mapped to an Object property (via getter, constructor argument or field).
Succinctly whenever there is a property in the JSON that has not been mapped to its java / DTO object, Jackson will throw this exception.
So What Could Have Happened?
The service provider your application is consuming has added a new attribute in the return of the service. Since such an attribute does not exist in your java / DTO object, we have an unrecognized property, making the deserialization process impossible (JSON -> object).
Ok, Sherlock! And now?
To paraphrase, and correct myself: whenever there is a property in JSON that has not been mapped to its java / DTO object, Jackson will throw this exception, unless you tell Jackson that he can ignore such attributes.
Ignoring Unknown Fields with Jackson
Fortunately, there are two ways to work around the problem in question and avoid throwing the exception:
- Annotate the class with @JsonIgnoreProperties (ignoreUnknown = true)
- Set the Deserialization Feature FAIL_ON_UNKNOWN_PROPERTIES to false
@JsonIgnoreProperties(ignoreUnknown=true)
Adding to your class @JsonIgnoreProperties(ignoreUnknown = true) annotation will tell Jackson to ignore unknown attributes when deserializing JSONs to objects in that class.
@JsonIgnoreProperties(ignoreUnknown=true)
public class AnnotatedPersonDto {
private String name;
private String sex;
// ...
}
Set the Deserialization Feature FAIL_ON_UNKNOWN_PROPERTIES to false
Setting up the object mapper will tell Jackson to ignore unknown attributes in all deserializations where that object mapper is used.
// version 1.9 or before
objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// version 2.0 or after
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Let's Go to Tests
For our tests, we will need two DTO’s, one without an annotation (to prove the exception was thrown) and another with an annotation (to prove the resolution of the problem).
public class UnannotatedPersonDto {
private String name;
private String sex;
// ...
}
@JsonIgnoreProperties(ignoreUnknown=true)
public class AnnotatedPersonDto {
private String name;
private String sex;
// ...
}
The JSON below will be used, which has the age attribute that is not known by the DTO.
{ "name": "LINUS" , "age": 18, "sex": "MALE" }
Below we have 3 tests:
@SpringBootTest
class JacksonIgnorePropertiesTests {
private String JSON_TO_DESERIALIZE = "{ \"name\": \"LINUS\" , \"age\": 18, \"sex\": \"MALE\" }";
@Test
void withJsonWithUnknownAttributes_whenWithoutAnnotationOrConfiguration_thenThrownException() throws JsonMappingException, JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
assertThrows(UnrecognizedPropertyException.class, () -> { mapper.readValue(JSON_TO_DESERIALIZE, UnannotatedPersonDto.class); });
}
@Test
void withJsonWithUnknownAttributes_whenDtoHasAnnotationJsonIgnoreProperties_thenWillDeserialize() throws JsonMappingException, JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
assertEquals("LINUS", mapper.readValue(JSON_TO_DESERIALIZE, AnnotatedPersonDto.class).getNome());
}
@Test
void withJsonWithUnknownAttributes_whenObjectMapperIsConfigured_thenWillDeserialize() throws JsonMappingException, JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
assertEquals("LINUS", mapper.readValue(JSON_TO_DESERIALIZE, UnannotatedPersonDto.class).getNome());
}
}
Results:
The exception UnrecognizedPropertyException is thrown once JSON is deserialized using the class without annotation and no configuration has been added to the Object Mapper;
Deserialization is successful since the DTO that has the annotation used to ignore unknown attributes;
Deserialization also occurs successfully because despite using the DTO without the annotation to ignore unknown attributes, the object mapper was configured with the FAIL_ON_UNKNOWN_PROPERTIES feature that ignores those attributes.
Conclusion
So what is the best approach? It depends.
The approach of annotating classes with @JsonIgnoreProperties allows for finer control over which objects should ignore unknown fields and which should not. On the other hand, a developer may forget to put the annotation to a class, and then the problem could occur.
The approach of configuring the object mapper in line with a dependency injection framework, ensuring that the same object