Search code examples
javajsoninheritancejackson

JSON schema shared fields between multiple objects


I have the following schemas for JSON objects for some endpoints:

{ // Dog.java
  "name": "Fido",
  "age": 5,
  "barkLoudness": 8,
  "treatsToday": 2
}
{ // Cat.java
  "name": "Fluffy",
  "age": 2,
  "color": "white",
}
{ // Fish.java
  "name": "Professor",
  "age": 1,
  "blub": "blub"
}

All of the schemas share the name and age field, but can extend it with their own properties. When deserializing, the object mapper will know what type its deserializing too, such as a POST to /dog invoking mapper.readValue(input, Dog.class). What I'm struggling with is how to define the classes, such that I don't have to repeat the name and age field with all of them:

abstract class Animal<T> { // abstract? interface? concrete?
  private final String name;
  private final int age;
  @JsonUnwrapped
  private final T info;

  // unsure if Unwrapped works here
  @JsonCreator
  protected Animal(
    @JsonProperty("name") String name,
    @JsonProperty("age") int age,
    @JsonUnwrapped T info 
  ) {
    this.name = name;
    this.age = age;
    this.info = info;
  }
  // getters
}

public class Dog extends Animal<DogInfo> {
  private final int barkLoudness;
  private final int treatsToday;
  // Don't want to have to repeat the name/age in concrete subtypes
  // or the JsonProperty annotation 
  @JsonCreator
  public Dog(
    @JsonProperty("name") String name, 
    @JsonProperty("age") int age, 
    @JsonProperty("barkLoudness") int barkLoudness, 
    @JsonProperty("treatsToday") int treatsToday
  ) {
    super(name, age);
    this.barkLoudness = barkLoudness;
    this.treatsToday = treatsToday;
  }
  // getters
}

If it helps, I'm never going to have a method such as process(Animal<T> animal), I'll always be working with a concrete type. I want to make all objects immutable, and to not repeat the age and name fields/definition between all the objects. I have a small preference to not use the @JsonTypeInfo annotation, but if that's the solution I'm fine with it. Is there a way to create the class structure so this "inheritance" can be achieved?


Solution

  • Using inheritance and the @JsonProperty annotation, you can prevent having to repeat age and name between all the objects:

    import com.fasterxml.jackson.annotation.JsonCreator;
    import com.fasterxml.jackson.annotation.JsonProperty;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    abstract class Animal {
      private final String name;
      private final int age;
    
      @JsonCreator
      protected Animal(@JsonProperty("name") String name, @JsonProperty("age") int age) {
        this.name = name;
        this.age = age;
      }
    
      public String getName() {
        return name;
      }
    
      public int getAge() {
        return age;
      }
    }
    
    final class Dog extends Animal {
      private final int barkLoudness;
      private final int treatsToday;
    
      @JsonCreator
      public Dog(
        @JsonProperty("name") String name, 
        @JsonProperty("age") int age, 
        @JsonProperty("barkLoudness") int barkLoudness, 
        @JsonProperty("treatsToday") int treatsToday
      ) {
        super(name, age);
        this.barkLoudness = barkLoudness;
        this.treatsToday = treatsToday;
      }
    
      public int getBarkLoudness() {
        return barkLoudness;
      }
    
      public int getTreatsToday() {
        return treatsToday;
      }
    }
    
    final class Cat extends Animal {
      private final String color;
    
      @JsonCreator
      public Cat(
        @JsonProperty("name") String name, 
        @JsonProperty("age") int age, 
        @JsonProperty("color") String color
      ) {
        super(name, age);
        this.color = color;
      }
    
      public String getColor() {
        return color;
      }
    }
    
    final class Fish extends Animal {
      private final String blub;
    
      @JsonCreator
      public Fish(
        @JsonProperty("name") String name, 
        @JsonProperty("age") int age, 
        @JsonProperty("blub") String blub
      ) {
        super(name, age);
        this.blub = blub;
      }
    
      public String getBlub() {
        return blub;
      }
    }
    
    public class Main {
      public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
    
        String dogJson = """
        {
          "name": "Fido",
          "age": 5,
          "barkLoudness": 8,
          "treatsToday": 2
        }
        """;
        Dog dog = mapper.readValue(dogJson, Dog.class);
        System.out.println("Dog: " + dog.getName() + ", Age: " + dog.getAge() + 
                   ", Bark Loudness: " + dog.getBarkLoudness() + 
                   ", Treats Today: " + dog.getTreatsToday());
    
        String catJson = """
        {
          "name": "Fluffy",
          "age": 2,
          "color": "white"
        }
        """;
        Cat cat = mapper.readValue(catJson, Cat.class);
        System.out.println("Cat: " + cat.getName() + ", Age: " + cat.getAge() + 
                   ", Color: " + cat.getColor());
    
        String fishJson = """
        {
          "name": "Professor",
          "age": 1,
          "blub": "blub"
        }
        """;
        Fish fish = mapper.readValue(fishJson, Fish.class);
        System.out.println("Fish: " + fish.getName() + ", Age: " + fish.getAge() + 
                   ", Blub: " + fish.getBlub());
      }
    }
    

    Output:

    Dog: Fido, Age: 5, Bark Loudness: 8, Treats Today: 2
    Cat: Fluffy, Age: 2, Color: white
    Fish: Professor, Age: 1, Blub: blub