Search code examples
javajsonjacksonfasterxml

Working with multiple JSON objects in the same file with unknown key names using Jackson


Working on building the model for an application dealing with physical buildings.

Ideally, we'd want something like this:

City has multiple Offices, which have multiple Rooms, which have properties.

We're using jackson to parse the JSON payload received from the API datasource, and it ends up looking a bit differently than the examples I've seen.

The format we're getting is:

{
"CityName1": 
    { "OfficeName1": 
        [   
            {"name": RoomName1, "RoomProperty2": RoomValue1},
            {"name": RoomName2, "RoomProperty2": RoomValue2}
        ]
    }, 
    { "OfficeName2": [{...}]},
    { "OfficeNameX" : [{...}] },
"CityName2": {...},
"CityNameN": {...}}

Java classes:

public class City {
  private Map<String, Object> additionalProperties = new HashMap<String, Object();

  private List<Office> _offices = new ArrayList<Office>();

  @JsonAnyGetter
  public Map<String, Object> getAdditionalProperties() {
    return this.additionalProperties;
  }

  @JsonAnySetter
  public void setAdditionalProperty(String name, Object value)
      throws IOException {
    _cityName = name;
    String officeJson = _mapper.writeValueAsString(value);
    StringBuilder sb = new StringBuilder(officeJson);
    _offices.add(_mapper.readValue(officeJson, Office.class));
    this.additionalProperties.put(name, value);
  }
}

public class Office {

  private String _officeName;

  private static final ObjectMapper _mapper = new ObjectMapper();

  private Map<String, Object> additionalProperties = new HashMap<String, Object>();

  private List<Room> _rooms = new ArrayList<Room>();

  @JsonAnyGetter
  public Map<String, Object> getAdditionalProperties() {
    return this.additionalProperties;
  }

  @JsonAnySetter
  public void setAdditionalProperty(String name, Object value)
      throws IOException {
    _officeName = name;
    String roomJson = _mapper.writeValueAsString(value);
    Room[] rooms  = _mapper.readValue(roomJson, Room[].class);
    _rooms.addAll(Arrays.asList(rooms));
    this.additionalProperties.put(name, value);
  }

  public List<Room> getRooms() {
    return _rooms;
  }

  public void setRooms(List<Room> rooms) {
    _rooms = rooms;
  }  
}

public class Room {

  private static final String NAME = "name";
  private static final String PROP_2 = "RoomProperty2";

  @JsonProperty(PROP_2)
  private String _propertyTwo;

  @JsonProperty(NAME)
  private String name;

  @JsonProperty(PROP_2)
  public String getPropertyTwo() {
    return _propertyTwo;
  }

  @JsonProperty(PROP_2)
  public void setPropertyTwo(String propTwo) {
    _propertyTwo = propTwo;
  }

  public String getName() {
    return name;
  }

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

So how would I go about parsing this with jackson ? Currently, I am using an @JsonAnySetter to grab the name, and saving that as the city or office name and then sending the value sent to JsonAnySetter to the appropriate nested class. The real issue comes with getting a list of Offices in the City. When using a mapper.readvalues(String, Office.class), I get returned an iterator of only the last office for each city. Any ideas guys?

Sorry if that seemed confusing! Would love to answer any questions I've created.

Thanks for the help!


Solution

  • I think the best solution is to write your own deserialiser here since your JSON document doesn't really map well to the class structure you want.

    The solution below reads each city as a Map<String, List<Room>> and the collection of cities as a Map<String, City> and then create City and Office objects from these inside the deserialisers.

    Room.java is the same as yours, here are the rest:

    Cities.java:

    @JsonDeserialize(using=CitiesDeserializer.class)
    public class Cities implements Iterable<City> {
    
        private final List<City> cities;
    
        public Cities(final List<City> cities) {
            this.cities = cities;
        }
    
        public Cities() {
            this.cities = new ArrayList<>();
        }
    
        public List<City> getCities() {
            return cities;
        }
    
        @Override
        public Iterator<City> iterator() {
            return cities.iterator();
        }
    }
    

    CitiesDeserialiser.java:

    public class CitiesDeserializer extends JsonDeserializer<Cities> {
        private static final TypeReference<Map<String, City>> TYPE_REFERENCE = new TypeReference<Map<String, City>>() {};
    
        @Override
        public Cities deserialize(final JsonParser jp, final DeserializationContext ctxt) throws IOException {
            final Map<String, City> map = jp.readValueAs(TYPE_REFERENCE);
            List<City> cities = new ArrayList<>();
            for(Map.Entry<String, City> entry : map.entrySet()) {
                City city = entry.getValue();
                city.setName(entry.getKey());
                cities.add(city);
            }
            return new Cities(cities);
        }
    }
    

    City.java:

    @JsonDeserialize(using=CityDeserialzer.class)
    public class City {
    
        private String name;
    
        private List<Office> offices;
    
        // Setters and getters
    }
    

    CityDeserializer.java:

    public class CityDeserialzer extends JsonDeserializer<City> {
        private static final TypeReference<Map<String, List<Room>>> TYPE_REFERENCE = new TypeReference<Map<String, List<Room>>>() {};
    
        @Override
        public City deserialize(final JsonParser jp, final DeserializationContext ctxt) throws IOException {
            final Map<String, List<Room>> map = jp.readValueAs(TYPE_REFERENCE);
            List<Office> offices = new ArrayList<>();
            for(Map.Entry<String, List<Room>> entry : map.entrySet()) {
                Office office = new Office();
                office.setName(entry.getKey());
                office.setRooms(entry.getValue());
                offices.add(office);
            }
            City city = new City();
            city.setOffices(offices);
            return city;
        }
    }
    

    Office.java:

    public class Office {
    
        private String name;
    
        private List<Room> rooms;
    
        // Setters and getters
    }
    

    And here's a test to show that it works:

    JSON:

    {
        "CityName1": {
            "OfficeName1": [ {
                    "name": "RoomName1",
                    "RoomProperty2": "RoomValue1"
                }, {
                    "name": "RoomName2",
                    "RoomProperty2": "RoomValue2"
                } ],
            "OfficeName2": [ {
                    "name": "RoomName3",
                    "RoomProperty2": "RoomValue3"
                }, {
                    "name": "RoomName4",
                    "RoomProperty2": "RoomValue4"
                } ]
        },
        "CityName2": {
            "OfficeName3": [ {
                    "name": "RoomName5",
                    "RoomProperty2": "RoomValue5"
                }, {
                    "name": "RoomName6",
                    "RoomProperty2": "RoomValue6"
                } ],
            "OfficeName4": [ {
                    "name": "RoomName7",
                    "RoomProperty2": "RoomValue7"
                }, {
                    "name": "RoomName8",
                    "RoomProperty2": "RoomValue8"
                } ]
        }
    }
    

    Test.java:

    public class Test {
    
        public static void main(String[] args) {
            String json = ...
            ObjectMapper mapper = new ObjectMapper();
            Cities cities = mapper.readValue(json, Cities.class);
            for(City city : cities) {
                System.out.println(city.getName());
                for(Office office : city.getOffices()) {
                    System.out.println("\t" + office.getName());
                    for(Room room : office.getRooms()) {
                        System.out.println("\t\t" + room.getName());
                        System.out.println("\t\t\t" + room.getPropertyTwo());
                    }
                }
            }
        }
    }
    

    Output:

    CityName1
        OfficeName1
            RoomName1
                RoomValue1
            RoomName2
                RoomValue2
        OfficeName2
            RoomName3
                RoomValue3
            RoomName4
                RoomValue4
    CityName2
        OfficeName3
            RoomName5
                RoomValue5
            RoomName6
                RoomValue6
        OfficeName4
            RoomName7
                RoomValue7
            RoomName8
                RoomValue8