2

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?

buræquete
  • 14,226
  • 4
  • 44
  • 89
Kirill
  • 6,762
  • 4
  • 51
  • 81
  • Possible duplicate of [Deserialize JSON to ArrayList using Jackson](https://stackoverflow.com/questions/9829403/deserialize-json-to-arraylistpojo-using-jackson) – Daniel Eisenreich Dec 20 '17 at 06:33
  • I think that deserializing to an array of arrays and then converting might be easier. – Kraylog Dec 20 '17 at 06:47

2 Answers2

4

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));
Kirill
  • 6,762
  • 4
  • 51
  • 81
1

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
}
buræquete
  • 14,226
  • 4
  • 44
  • 89
  • Thank you. I was able to do the trick even with `@JsonCreator`. Wrote custom deserializer, then created new `org.codehaus.jackson.map.module.SimpleModule`, then `module.addDeserializer(Person.class, new PersonDeserializer())` and registered the module with objectmapper: `mapper.registerModule(module)` – Kirill Dec 20 '17 at 08:15
  • 1
    @Derp I see, I wouldn't have gone that far ahead :), because I think, it is better to know from the bean that there is a custom logic present, by adding a module you hide that logic, however it looks cleaner, it might be confusing. Still I am glad that it was possible through a custom module, you should add that as an answer! – buræquete Dec 20 '17 at 08:23
  • 1
    Added my solution as an answer. `@JsonCreator` is not actually required, custom deserializer does the job not looking at it :) – Kirill Dec 21 '17 at 09:27