THIS USER NEEDS HELP THIS USER NEEDS HELP - 3 months ago 38
JSON Question

Conditional field requirement based on another field value in Jackson?

Consider a JSON representation with one string and two arrays. For example,

{
"type" : "A",
"ListA" : []
"ListB" : [3, 4, 5]
}


In the above case,
type
is required field, but
ListA
and
ListB
are conditionally required for deserialization based on the value of
type
. In other words,
ListA
is only required if
type
had value
A
and
ListB
is only required if
type
had a value
B
.

Currently, I am working in Jackson and in Java, and I have been able to implement making
type
field mandatory by creating
POJO
as following:

public class Example {
@JsonProperty(required = true)
String type;

// getter and setter auto-generated


But I can't just attach another
@JsonProperty(required = true)
to
ListA
or
ListB
since it's dependent on the value of
type
.

How can I conditionally require
ListA
and
ListB
for deserialization based on the value of
type
?

Also, I will be performing additional checks such as whether either
ListA
or
ListB
is an empty array (
size == 0
) or not.

Answer

You could use a custom deserializer to achieve it.

Defining your model

Your Example class would be like:

public class Example {

    private String type;
    private List<Integer> listA;
    private List<Integer> listB;

    // Getters and setters ommited    
}

Creating a custom deserializer

Your custom deserializer could be as follwing:

public class ExampleDeserializer extends StdDeserializer<Example> {

    private static final String TYPE_A = "A";
    private static final String TYPE_B = "B";

    public ExampleDeserializer() {
        super(Example.class);
    }

    @Override
    public Example deserialize(JsonParser p, DeserializationContext ctxt) 
                   throws IOException, JsonProcessingException {

        ObjectMapper mapper = (ObjectMapper) p.getCodec();  
        JsonNode tree = mapper.readTree(p);  

        Example example = new Example();

        JsonNode typeNode = tree.get("type");
        if (typeNode == null || typeNode.asText().isEmpty()) {
            throw ctxt.mappingException("\"type\" is required");
        }
        example.setType(typeNode.asText());

        switch (typeNode.asText()) {

        case TYPE_A:
            ArrayNode listANode = (ArrayNode) tree.get("ListA");
            if (listANode == null || listANode.size() == 0) {
                throw ctxt.mappingException(
                           "\"ListA\" is required when \"type\" is \"" + TYPE_A + "\"");
            }
            example.setListA(createList(listANode));
            break;

        case TYPE_B:
            ArrayNode listBNode = (ArrayNode) tree.get("ListB");
            if (listBNode == null || listBNode.size() == 0) {
                throw ctxt.mappingException(
                           "\"ListB\" is required when \"type\" is \"" + TYPE_B + "\"");
            }
            example.setListB(createList(listBNode));
            break;

        default:
            throw ctxt.mappingException(
                       "\"type\" must be \"" + TYPE_A + "\" or \"" + TYPE_B + "\"");
        }


        return example;
    }

    private List<Integer> createList(ArrayNode arrayNode) {
        List<Integer> list = new ArrayList<Integer>();
        for (JsonNode node : arrayNode) {
            list.add(node.asInt());
        }
        return list;
    }
}

Registering the custom deserializer

Register the custom deserializer defined above to your ObjectMapper:

SimpleModule module = new SimpleModule("ExampleDeserializer", 
        new Version(1, 0, 0, null, "com.example", "example-deserializer")); 

ExampleDeserializer exampleDeserializer = new ExampleDeserializer();
module.addDeserializer(Example.class, exampleDeserializer);

ObjectMapper mapper = new ObjectMapper()
                          .registerModule(module)
                          .enable(SerializationFeature.INDENT_OUTPUT);

Testing your custom deserializer

Use the custom serializer:

String json = "{\"type\":\"A\",\"ListA\":[1,2,3]}";
Example example = mapper.readValue(json, Example.class);