13

JSON

{
  "schools": [
    {
      "id": 1,
      "name": "School A"
    },
    {
      "id": 2,
      "name": "School B"
    }
  ],
  "students": [
    {
      "id": 1,
      "name": "Bobby",
      "school": 1
    }
  ]

}

How would I map the JSON into the following classes such that Bobby's school is mapped to the already instantiated School A.

public class School {
  private Integer id;
  private String name;
}

public class Student {
  private Integer id;
  private String name;
  private School school;
}

I've tried some weird stuff with the Student class...

public class Student {
  private Integer id;
  private String name;
  private School school;

  @JsonProperty("school")
  public void setSchool(Integer sid) {
    for (School school : getSchools()) {
      if (school.id == sid) {
        this.school = school;
        break;
      }
    }
  }
}

The problem I'm having is that both the schools and the students are being parsed from the JSON at the same time, so I'm not sure how to get a list of the schools. Maybe I should parse these separately so I have the list of schools first?

Rawr
  • 2,206
  • 3
  • 25
  • 53
  • Have you tried something? It's not clear what you're stuck on. – 4castle Dec 11 '16 at 00:19
  • Sorry about that, I've tried to better explain the issue I'm trying to solve with an example of one of the approaches I've been trying to take. – Rawr Dec 11 '16 at 00:30

2 Answers2

6

Jackson will do it for you. Just annotate your objects with @JsonIdentityInfo:

@JsonIdentityInfo(scope=School.class, generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
public class School {
    private Integer id;
    private String name;

    public School() {
    }

    public School(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

@JsonIdentityInfo(scope=Student.class, generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
public class Student {
    private Integer id;
    private String name;
    private School school;

    public Student() {
    }

    public Student(Integer id, String name, School school) {
        this.id = id;
        this.name = name;
        this.school = school;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public School getSchool() {
        return school;
    }

    public void setSchool(School school) {
        this.school = school;
    }
}

public static void main(String[] args) throws IOException {
    School school = new School(1, "St Magdalene's");
    Student mary = new Student(1, "Mary", school);
    Student bob = new Student(2, "Bob", school);
    Student[] students = new Student[] {mary, bob};

    // Write out
    String serialized = mapper.writeValueAsString(students);
    System.out.println("Serialized: " + serialized);
    // Read in
    Student[] deserialized = mapper.readValue(serialized, Student[].class);
}
teppic
  • 7,051
  • 1
  • 29
  • 35
  • 2
    Didn't work. Serialized it looks like: `[{"id":1,"name":"Mary","school":{"id":1,"name":"St Magdalene's"}},{"id":2,"name":"Bob","school":1}]` Deserialized it doesn't link to the school object but rather stored only the id of the school. – Rawr Dec 11 '16 at 05:24
  • Sorry, I should have checked the result. Without type info the deserialization was to a list of hash sets. I've updated the example to use a Student[] so that Jackson has enough info to build the objects. – teppic Dec 11 '16 at 05:43
  • As an alternative, you can let Jackson manage the ids. Remove the `id` properties and use `generator= ObjectIdGenerators .IntSequenceGenerator`. – teppic Dec 11 '16 at 05:46
  • I had deserialization issue (`JsonMappingException: Unexpected end-of-input: was expecting closing`) when using the default property `@id`, so I had to use another one. Note that the property name defined in the `@JsonIdentityInfo` is not required to exist in the Java object (you can define any name, e.g. `myJacksonId`). In the end I used `@JsonIdentityInfo(scope=School.class, generator= ObjectIdGenerators.IntSequenceGenerator.class, property = "myJacksonId")` only on the `School` class (it's not required on the `Student` class). – Julien Kronegg Jan 10 '19 at 12:17
3

With classes defined as below:

@JsonIdentityInfo(property = "id", generator = ObjectIdGenerators.PropertyGenerator.class)
class School {
    public Integer id;
    public String name;
}

class Student {
    public Integer id;
    public String name;
    @JsonIdentityReference(alwaysAsId = true)
    public School school;
}

class All {
    public List<School> schools;
    public List<Student> students;
}

This works exactly as You intended:

@Test
public void test() throws JsonProcessingException {
    var json = "{" +
            "\"schools\":[" +
            "{\"id\":1,\"name\":\"School A\"}," +
            "{\"id\":2,\"name\":\"School B\"}" +
            "]," +
            "\"students\":[" +
            "{\"id\":1,\"name\":\"Bobby\",\"school\":1}" +
            "]" +
            "}";
    var mapper = new ObjectMapper();
    var all =  mapper.readValue(json, All.class);
    Assertions.assertThat(all.students.get(0).school).isSameAs(all.schools.get(0));
}
Tomasz Gawel
  • 8,379
  • 4
  • 36
  • 61