1. Overview
When working with external APIs or evolving systems, JSON data often changes over time, with new fields appearing in responses. If not handled properly, these differences can cause deserialization to fail and interrupt the data flow.
In this lesson, we’ll explore how to make applications more resilient to such cases by using the mechanisms Jackson provides for handling unknown properties.
The relevant module we need to import when starting this lesson is: handling-unknown-properties-start.
If we want to reference the fully implemented lesson, we can import: handling-unknown-properties-end.
2. The Problem in Practice: Failing on an Unknown Property
Imagine that a brand is running multiple campaigns for its promotions and wants to also record the budget for each campaign for their reference, but our API hasn’t been updated to support a property for the budget data. Trying to deserialize a JSON with unknown properties will lead to a UnrecognizedPropertyException.
Let’s open our JacksonUnitTest class and add a new method to demonstrate this default ObjectMapper behavior:
@Test
void givenUnknownProperty_whenUsingDefaultMapper_thenFail() {
ObjectMapper mapper = new ObjectMapper();
String json = """
{
"code": "C2",
"name": "Campaign 2",
"description": "The description of Campaign 2",
"budget": 100
}
""";
assertThrows(UnrecognizedPropertyException.class, () -> mapper.readValue(json, Campaign.class));
}
Running this test confirms Jackson’s default behavior — deserialization fails with an UnrecognizedPropertyException when unknown fields are present.
3. Handling Unknown Properties Using the ObjectMapper
The quickest way to handle any changes in the JSON is to use a configuration of the ObjectMapper while deserializing.
When we configure FAIL_ON_UNKNOWN_PROPERTIES as false, we can ignore unknown fields received in JSON. This is ideal for public APIs that evolve very quickly, and when we’re confident that silently dropping extra fields won’t have any effect.
Let’s configure a new ObjectMapper with this configuration and then try to deserialize a JSON that includes the unknown property budget:
@Test
void givenMapperConfiguredToIgnoreUnknown_thenDeserializationSucceeds() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
String json = """
{
"code": "C2",
"name": "Campaign 2",
"description": "The description of Campaign 2",
"budget": 500
}
""";
Campaign campaign = mapper.readValue(json, Campaign.class);
assertEquals("C2", campaign.getCode());
}
When running this test, we’ll be able to deserialize this JSON. Jackson will quietly drop the unknown property.
Keep in mind that this configuration applies to the entire ObjectMapper instance. All deserialization done with this mapper will ignore unknown properties.
4. Class-Level Control with @JsonIgnoreProperties
Applying configuration globally is not always a feasible solution. Consider our Task class. If we apply this global configuration, then deserialization will start ignoring unknown properties in Task objects as well, which we might not want.
Since the third-party service doesn’t know whether we’re consuming newly added properties or not, this causes inconsistency. We want that deserialization to fail if an unknown property is present. This is where the @JsonIgnoreProperties annotation comes in handy.
Adding @JsonIgnoreProperties(ignoreUnknown = true) to a class will allow us to ignore the unknown properties in that single class only.
Let’s create a new class called CampaignWithIgnoreUnknown that extends our Campaign class:
@JsonIgnoreProperties(ignoreUnknown = true)
public class CampaignWithIgnoreUnknown extends Campaign{
}
Now, let’s try to deserialize a JSON string with an unknown budget property using the default ObjectMapper:
@Test
void givenJsonIgnorePropertiesConfiguredToIgnoreUnknown_thenDeserializationSucceeds() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
String json = """
{
"code": "C2",
"name": "Campaign 2",
"description": "The description of Campaign 2",
"budget": 500
}
""";
CampaignWithIgnoreUnknown campaign = mapper.readValue(json, CampaignWithIgnoreUnknown.class);
assertEquals("C2", campaign.getCode());
}
Since we’ve configured the CampaignWithIgnoreUnknown class to ignore unknown properties, the JSON is successfully deserialized. This approach allows us to ignore unknown properties for exactly one class.
Furthermore, as we cover in another lesson, the @JsonIgnoreProperties annotation also allows us to list specific property names to ignore, whether during serialization or deserialization. This gives us fine-grained control to exclude only the fields we choose.
5. Capturing Unknown Fields with @JsonAnySetter
We’ve seen that we can discard the unknown properties, but sometimes discarding new properties is not enough. We may need to store them for auditing, logging, or round-tripping the original payload. Using @JsonAnySetter designates a method that receives every property Jackson cannot match, and those unknown properties can be saved in a Map for our future reference.
Let’s create a new class called CampaignWithSetUnknownProperties that extends the Campaign class and adds a Map attribute and a method to save the unknown properties:
public class CampaignWithSetUnknownProperties extends Campaign {
private Map<String, Object> unknownProperties = new HashMap<>();
@JsonAnySetter
void addUnknownProperties(String key, Object value) {
unknownProperties.put(key, value);
}
public Map<String, Object> getUnknownProperties() {
return unknownProperties;
}
}
Now, let’s add a test method to see the same behavior in action:
@Test
void givenJsonAnySetterConfiguredToRecordUnknown_thenDeserializationSucceeds() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
String json = """
{
"code": "C2",
"name": "Campaign 2",
"description": "The description of Campaign 2",
"budget": 500
}
""";
CampaignWithSetUnknownProperties campaign = mapper.readValue(json, CampaignWithSetUnknownProperties.class);
assertTrue(campaign.getUnknownProperties().containsKey("budget"));
}
Here, the budget field will be saved in the unknownProperties map.
Note: The setter method must accept two arguments — a String for the property name and a second parameter for the value (often Object, but Jackson will attempt type conversion if you use something more specific). Alternatively, we can annotate the Map<String, Object> unknownProperties field itself with @JsonAnySetter, in which case Jackson will populate it directly without needing a separate method.
This strategy allows us to preserve every property of the original message, which is useful when forwarding data to another service, generating audit logs, or gradually migrating to a newer JSON schema.