Json schema with anyOf fields to POJO

Viewed 2368

I wonder what's the recommended way to generate POJOs for Json schemas with "anyOf" fields?

For example, given the following json schemas:

hobby.json
{
    "anyOf": [
        { "type": {"$ref": "./exercise.json" } },
        { "type": {"$ref": "./music.json" } }
    ]    
}
exercise.json
{
    "type": "object"
    "properties" {
        "hobbyType": {"type": "string"}
        "exerciseName": { "type": "string" },
        "timeSpent": { "type": "number" },
        "place": { "type": "string" }
    }
}
music.json
{
    "type": "object"
    "properties" {
        "hobbyType": {"type": "string"}
        "instrument": { "type": "string" },
        "timeSpent": { "type": "number" }
    }
}

How could I generate a POJO for Hobby.java with Jackson?

2 Answers

I think there are two approaches that seem natural:

One would be to generate a class hierarchy Hobby with the common field timeSpent and Music / Exercise being subclasses with their specific fields.

The other would be to "union" the fields into a single class Hobby.

Both are semantically incorrect meaning that you can come up with cases where JSON schema validates correctly but Jackson throws an error or the information is missing in the POJO due to an omitted field.

So I think the best approach here would be to resort to Map<String, Object> instead of pojos.

So for example if a Person has a hobby the Person POJO could be:

class Person {
  String name;
  ...
  Map<String, Object> hobby;

or List<Map<String, Object> hobbies> if one can have multiple hobbies.

The approach I ended up taking is using polymorphic marshaling/unmarshaling functionality provided by Jackson.

Specifically -

  1. Make hobby to be an interface and annotate it with @JsonTypeInfo and @JsonSubTypes
@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    property = "hobbyType",
    include = JsonTypeInfo.As.EXISTING_PROPERTY,
    visible = true
)
@JsonSubTypes({
        @Type(value = Exercise.class, name = "exercise"),
        @Type(value = Music.class, name = "music")
})
public interface Hobby {
}
  1. Create Exercise.java and Music.java that implements this interface
@Builder
@Data
@AllArgsConstructor
public class Exercise implements Hobby {
    @JsonProperty("hobbyType")
    @Builder.Default
    @NonNull
    private final String hobbyType = "exercise";    

    @JsonProperty("exerciseName")
    private String exerciseName;

    @JsonProperty("place")
    private String place;
    //... (other fields)
}
  1. Use Hobby for serialization and deserialization.
// create a Hobby object
Hobby exercise = Exercise.builder().exerciseName("swimming").place("swimmingPool").build();

// serialization
String serializedHobby = new ObjectMapper.writeValueAsString(exercise)
/**
serializedHobby looks like this ->
{
    "hobbyType": "exercise",
    "exerciseName": "swimming",
    "place": "swimmingPool"
}
*/

// deserialization
Hobby deserializedObject = new ObjectMapper.readValue(jsonString, Hobby.class)
// deserializedObject.getClass() would return Exercise.java or Music.java based on the hobbyType

Ref: https://www.baeldung.com/jackson-inheritance

Related