5

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?

Liutong Chen
  • 2,915
  • 5
  • 22
  • 29

2 Answers2

0

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.

aeberhart
  • 744
  • 1
  • 4
  • 15
0

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

Liutong Chen
  • 2,915
  • 5
  • 22
  • 29
  • What would your classes look like if it was "oneOf" or "allOf" instead of "anyOf"? – aeberhart Aug 12 '20 at 07:56
  • @aeberhart I think it depends on the structure of the schema. If it needs to deserialize/serialize in a polymorphic way, you can perhaps try this approach. – Liutong Chen Aug 12 '20 at 17:26
  • Ok makes sense. So I guess your question is more about polymorphism rather than the differences between allOf, anyOf, oneOf, correct? – aeberhart Aug 13 '20 at 07:59
  • I guess you could say that. The way `anyOf` is used in my case is best to be resolved with polymorphic marshaling/unmarshaling – Liutong Chen Aug 13 '20 at 17:03