How to map a dynamic JSON property to a fixed POJO field - java

I have some json that i want to parse into pojo
{
"groups": [
{
"g1": [
1,2,5,6,7
]
},
{
"g2": [
2,3,48,79
]
}
]
}
Of course, g1 and g2 are the identifiers, so what i would imagine as pojos would be sth like
class Container {
List<Group> groups;
}
class Group {
String id;
List<Integer> values;
}
So it boils down to this question: How to use jackson to map a json-property to the pojo?

This kind of structure can be parsed using a custom deserializer added with the JsonDeserialize annotation.
POJOs
public static class Container {
private List<Group> groups;
public List<Group> getGroups() {
return groups;
}
public void setGroups(List<Group> groups) {
this.groups = groups;
}
#Override
public String toString() {
return String.format("Container [groups=%s]", groups);
}
}
#JsonDeserialize(using=CustomDeserializer.class)
public static class Group {
String id;
List<Integer> values;
#Override
public String toString() {
return String.format("Group [id=%s, values=%s]", id, values);
}
}
Deserializer, note use of ObjectMapper.readTree rather than using the low level JsonParser API...
public static class CustomDeserializer extends JsonDeserializer<Group> {
#Override
public Group deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
Group group = new Group();
ObjectNode objectNode = new ObjectMapper().readTree(jp);
// assume only a single field...
Entry<String, JsonNode> field = objectNode.fields().next();
group.id = field.getKey();
// there might be a nicer way to do this...
group.values = new ArrayList<Integer>();
for (JsonNode node : ((ArrayNode)field.getValue())) {
group.values.add(node.asInt());
}
return group;
}
}
Test
public static void main(String[] args) throws Exception {
String json = "{\"groups\": [{\"g1\":[1,2,5,6,7]},{\"g2\": [2,3,48,79]}]}";
JsonFactory f = new JsonFactory();
JsonParser jp = f.createParser(json);
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.readValue(jp, Container.class));
}
Output
Container [groups=[Group [id=g1, values=[1, 2, 5, 6, 7]], Group [id=g2, values=[2, 3, 48, 79]]]]

Related

Deserialize JSON array to a single Java object with Jackson

The idea is that I'd like to convert a JSON array ["foo", "bar"] into a Java object so I need to map each array element to property by index.
Suppose I have the following JSON:
{
"persons": [
[
"John",
"Doe"
],
[
"Jane",
"Doe"
]
]
}
As you can see each person is just an array where the first name is an element with index 0 and the last name is an element with index 1.
I would like to deserialize it to List<Person>.
I use mapper as follows:
mapper.getTypeFactory().constructCollectionType(List.class, Person.class)
where Person.class is:
public class Person {
public final String firstName;
public final String lastName;
#JsonCreator
public Person(#JsonProperty() String firstName, #JsonProperty String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
I was wondering if I can somehow specify array index as #JsonProperty argument instead of it's key name?
Thanks to bureaquete for suggestion to use custom Deserializer. But it was more suitable for me to register it with SimpleModule instead of #JsonDeserialize annotation. Below is complete JUnit test example:
#RunWith(JUnit4.class)
public class MapArrayToObjectTest {
private static ObjectMapper mapper;
#BeforeClass
public static void setUp() {
mapper = new ObjectMapper();
SimpleModule customModule = new SimpleModule("ExampleModule", new Version(0, 1, 0, null));
customModule.addDeserializer(Person.class, new PersonDeserializer());
mapper.registerModule(customModule);
}
#Test
public void wrapperDeserializationTest() throws IOException {
//language=JSON
final String inputJson = "{\"persons\": [[\"John\", \"Doe\"], [\"Jane\", \"Doe\"]]}";
PersonsListWrapper deserializedList = mapper.readValue(inputJson, PersonsListWrapper.class);
assertThat(deserializedList.persons.get(0).lastName, is(equalTo("Doe")));
assertThat(deserializedList.persons.get(1).firstName, is(equalTo("Jane")));
}
#Test
public void listDeserializationTest() throws IOException {
//language=JSON
final String inputJson = "[[\"John\", \"Doe\"], [\"Jane\", \"Doe\"]]";
List<Person> deserializedList = mapper.readValue(inputJson, mapper.getTypeFactory().constructCollectionType(List.class, Person.class));
assertThat(deserializedList.get(0).lastName, is(equalTo("Doe")));
assertThat(deserializedList.get(1).firstName, is(equalTo("Jane")));
}
}
class PersonsListWrapper {
public List<Person> persons;
}
class Person {
final String firstName;
final String lastName;
Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
class PersonDeserializer extends JsonDeserializer<Person> {
#Override
public Person deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
JsonNode node = jp.readValueAsTree();
return new Person(node.get(0).getTextValue(), node.get(1).getTextValue());
}
}
Note that if you do not need wrapper object, you can deserialize JSON array
[["John", "Doe"], ["Jane", "Doe"]] directly to List<Person> using mapper as follows:
List<Person> deserializedList = mapper.readValue(inputJson, mapper.getTypeFactory().constructCollectionType(List.class, Person.class));
It is easy to serialize, but not so easy to deserialize in such manner;
The following class can be serialized into an array of strings as in your question with #JsonValue;
public class Person {
private String firstName;
private String lastName;
//getter,setter,constructors
#JsonValue
public List<String> craeteArr() {
return Arrays.asList(this.firstName, this.lastName);
}
}
But to deserialize, I had to create a wrapper class, and use custom deserialization with #JsonDeserialize;
public class PersonWrapper {
#JsonDeserialize(using = CustomDeserializer.class)
private List<Person> persons;
//getter,setter,constructors
}
and the custom deserializer itself;
public class CustomDeserializer extends JsonDeserializer<List<Person>> {
#Override
public List<Person> deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException {
JsonNode node = jsonParser.readValueAsTree();
ObjectMapper mapper = new ObjectMapper();
return IntStream.range(0, node.size()).boxed()
.map(i -> {
try {
List<String> values = mapper.readValue(node.get(i).toString(), List.class);
return new Person().setFirstName(values.get(0)).setLastName(values.get(1));
} catch (IOException e) {
throw new RuntimeException();
}
}).collect(Collectors.toList());
}
}
You need to put proper validation in deserializer logic to check that each mini-array contains exactly two values, but this works well.
I'd rather use these steps, and maybe to hide #JsonDeserialize, I'd do the following;
#Retention(RetentionPolicy.RUNTIME)
#JacksonAnnotationsInside
#JsonDeserialize(using = CustomDeserializer.class)
public #interface AcceptPersonAsArray {}
So you can use some custom annotation in PersonWrapper
public class PersonWrapper {
#AcceptPersonAsArray
private List<Person> persons;
//getter,setter,constructors
}

Jackson catch unrecognized field in a map

I'm using Jackson in a java Rest Api to handle request params.
My Bean class :
public class ZoneModifBeanParam extends ModifBeanParam<Zone> {
#FormParam("type")
private String type;
#FormParam("geometry")
private Geometry geometry;
#FormParam("name")
private String name;
...
My API interface :
#POST
#Consumes("application/json")
#Produces("application/json; subtype=geojson")
#ApiOperation(value = "Create a zone", notes = "To create a zone")
public Response createZone(ZoneModifBeanParam zoneParam) {
...
This Works fine but I need to receive other params that aren't specified by my Bean in a Map.
Example :
{
"geometry": {...},
"name": "A circle name",
"type": "4",
"hello": true
}
By receiving this I need to store in a Map (named unrecognizedFields and declared in my bean) the couple ("hello", true).
Is there any annotation or object allowing this?
Just use #JsonAnySetter. That's what it's made for. Here is a test case
public class JacksonTest {
public static class Bean {
private String name;
public String getName() { return this.name; }
public void setName(String name) { this.name = name; }
private Map<String, Object> unrecognizedFields = new HashMap<>();
#JsonAnyGetter
public Map<String, Object> getUnrecognizedFields() {
return this.unrecognizedFields;
}
#JsonAnySetter
public void setUnrecognizedFields(String key, Object value) {
this.unrecognizedFields.put(key, value);
}
}
private final String json
= "{\"name\":\"paul\",\"age\":600,\"nickname\":\"peeskillet\"}";
private final ObjectMapper mapper = new ObjectMapper();
#Test
public void testDeserialization() throws Exception {
final Bean bean = mapper.readValue(json, Bean.class);
final Map<String, Object> unrecognizedFields = bean.getUnrecognizedFields();
assertEquals("paul", bean.getName());
assertEquals(600, unrecognizedFields.get("age"));
assertEquals("peeskillet", unrecognizedFields.get("nickname"));
}
}
The #JsonAnyGetter is used on the serialization side. When you serialize the bean, you will not see the unrecognizedFields in the JSON. Instead all the properties in the map will be serialized as top level properties in the JSON.
You may be able to ignore the unrecognized fields safely by configuring the ObjectMapper, however to specifically put them as key-value pairs of a Map field, you'll need your own de-serializer.
Here's a (heavily simplified) example:
Given your POJO...
#JsonDeserialize(using=MyDeserializer.class)
class Foo {
// no encapsulation for simplicity
public String name;
public int value;
public Map<Object, Object> unrecognized;
}
... and your custom de-serializer...
class MyDeserializer extends JsonDeserializer<Foo> {
#Override
public Foo deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
// new return object
Foo foo = new Foo();
// setting unrecognized container
Map<Object, Object> unrecognized = new HashMap<>();
foo.unrecognized = unrecognized;
// initializing parsing from root node
JsonNode node = p.getCodec().readTree(p);
// iterating node fields
Iterator<Entry<String, JsonNode>> it = node.fields();
while (it.hasNext()) {
Entry<String, JsonNode> child = it.next();
// assigning known fields
switch (child.getKey()) {
case "name": {
foo.name = child.getValue().asText();
break;
}
case "value": {
foo.value = child.getValue().asInt();
break;
}
// assigning unknown fields to map
default: {
foo.unrecognized.put(child.getKey(), child.getValue());
}
}
}
return foo;
}
}
Then, somewhere...
ObjectMapper om = new ObjectMapper();
Foo foo = om.readValue("{\"name\":\"foo\",\"value\":42,\"blah\":true}", Foo.class);
System.out.println(foo.unrecognized);
Output
{blah=true}

Jackson: Keep references to keys in map values when deserializing

I have the following JSON with a map from user IDs to user details:
{
"users": {
"john": { "firstName": "John", "lastName": "Doe" },
"mark": { "firstName": "Mark", "lastName": "Smith" }
}
}
and I'm using the following code to deserialize the JSON into a Java objects:
class User {
public String userID;
public String firstName;
public String lastName;
}
public class Users {
public Map<String, User> users;
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
Reader source = Files.newBufferedReader(Paths.get("test.json"));
Users all = mapper.readValue(source, Users.class);
// ...
}
}
After the deserialization, I want the field User.userID to be set to the corresponding key in the users map.
For example all.users.get("john").userID should be "john".
How can I do that?
Create a custom deserializer for User object and use this for the Map. Here's a full example:
#Test
public void test() throws JsonParseException, JsonMappingException, IOException {
ObjectMapper mapper = new ObjectMapper();
Data data = mapper.readValue("{\"users\": {\"John\": {\"id\": 20}, \"Pete\": {\"id\": 30}}}", Data.class);
assertEquals(20, data.users.get("John").id);
assertEquals(30, data.users.get("Pete").id);
assertEquals("John", data.users.get("John").name);
assertEquals("Pete", data.users.get("Pete").name);
}
public static class Data {
#JsonDeserialize(contentUsing = Deser.class)
public Map<String, User> users;
}
public static class User {
public String name;
public int id;
}
public static class Deser extends JsonDeserializer<User> {
#Override
public User deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String name = ctxt.getParser().getCurrentName();
User user = p.readValueAs(User.class);
user.name = name; // This copies the key name to the user object
return user;
}
}
The simplest solution for the problem is to implement a custom deserializer for the class in which you need the map key (see john16384's answer). This is however cumbersome if you have multiple maps with different value types in your JSON because you'd need one deserializer per type.
In this case, there is a better solution: I would create a custom #JsonMapKey annotation to mark the target properties for the map keys, and then register a generic custom deserializer that processes all occurrences of the annotation. These are the parts you need for this:
Custom #JsonMapKey annotation:
/**
* Annotation used to indicate that the annotated property shall be deserialized to the map key of
* the current object. Requires that the object is a deserialized map value.
*
* Note: This annotation is not a standard Jackson annotation. It will only work if this is
* explicitly enabled in the {#link ObjectMapper}.
*/
#Target({ ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
public #interface JsonMapKey {
}
Custom deserializer that processes the #JsonMapKey annotations:
public class JsonMapKeyDeserializer extends DelegatingDeserializer {
private static final long serialVersionUID = 1L;
private BeanDescription beanDescription;
public JsonMapKeyDeserializer(JsonDeserializer<?> delegate, BeanDescription beanDescription) {
super(delegate);
this.beanDescription = beanDescription;
}
#Override
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
return new JsonMapKeyDeserializer(newDelegatee, beanDescription);
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String mapKey = p.getCurrentName();
Object deserializedObject = super.deserialize(p, ctxt);
// set map key on all fields annotated with #JsonMapKey
for (BeanPropertyDefinition beanProperty : beanDescription.findProperties()) {
AnnotatedField field = beanProperty.getField();
if (field != null && field.getAnnotation(JsonMapKey.class) != null) {
field.setValue(deserializedObject, mapKey);
}
}
return deserializedObject;
}
}
Registration of the custom deserializer in the ObjectMapper:
private static void registerJsonMapKeyAnnotation(ObjectMapper objectMapper) {
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
BeanDescription beanDescription, JsonDeserializer<?> originalDeserializer) {
return new JsonMapKeyDeserializer(originalDeserializer, beanDescription);
}
});
objectMapper.registerModule(module);
}
Then you only need to annotate the field to be used for the map key...
class User {
#JsonMapKey
public String userID;
public String firstName;
public String lastName;
}
... and deserialize your JSON with the prepared ObjectMapper:
Users all = registerJsonMapKeyAnnotation(new ObjectMapper()).readValue(source, Users.class);
First Create the ObjectMapper class object than configure it.
Try following one.
Sample Code
Map<K, V> map;
ObjectMapper mapper = new ObjectMapper();
mapper.configure(Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
map = mapper.readValue(jsonStr, new TypeReference<Map<K, V>>() {});
than you can get the value using Map.

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!
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

How to map a Json attribute name to a Java field value

How to map a JSON attribute name to Java field value using jackson?
This:
{ "list":[ { "monday":[ "apple", "bread"] } ,
{ "sunday":[ "bacon", "beer" ] } ],
"kind":"shoplist" }
to this:
public class StuffList {
public List<Stuff> list;
public String kind;
}
public class Stuff {
public String name;
public List<String> items;
}
The fragment "monday":[ "apple", "bread"] is mapped to two variables, one with the attribute name and another with the attribute value.
Your JSON represets simple list of maps, more exactly: List<Map<String, List<String>>>. You can convert this JSON into this POJO:
class JsonEntity {
public List<Map<String, List<String>>> list;
public String kind;
public StuffList toStuffList() {
StuffList stuffList = createStuffListObject();
return stuffList;
}
private StuffList createStuffListObject() {
StuffList stuffList = new StuffList();
stuffList.kind = kind;
stuffList.list = createItemsList();
return stuffList;
}
private List<Stuff> createItemsList() {
List<Stuff> items = new ArrayList<Stuff>(list.size());
for (Map<String, List<String>> item : list) {
items.add(convertToStuff(item));
}
return items;
}
private Stuff convertToStuff(Map<String, List<String>> item) {
Stuff stuff = new Stuff();
stuff.name = item.keySet().iterator().next();
stuff.items = item.values().iterator().next();
return stuff;
}
}
And now we can deserialize JSON in this way:
public static void main(String[] args) throws Exception {
String json = "{\"list\":[{\"monday\":[\"apple\", \"bread\"]},{\"sunday\":[\"bacon\", \"beer\"]} ],\"kind\":\"shoplist\"}";
ObjectMapper objectMapper = new ObjectMapper();
JsonEntity jsonEntity = objectMapper.readValue(json, JsonEntity.class);
System.out.println(jsonEntity.toStuffList());
}
Output of the program:
list=[[name=monday, items=[apple, bread]], [name=sunday, items=[bacon, beer]]], kind=shoplist
Use annotation #JsonProperty If you want to change name use annotation with argument, e.g. #JsonProperty("stuff_name")

Categories