2

I am trying to parse the following JSON in Jackson:

{
  "x:y" : 1,
  "x:z" : 2,
  "u:v" : 3,
  // Several dynamically generated entries...
}

The data is formatted this way and outside of my control. The entries are somewhat dynamic, but always of the form:

"first:second" : value

I've been trying to serialize that into a container class:

private static class MyClass {
    String first;
    String second;
    Number value;

    @JsonCreator
    public MyClass(@JsonProperty("both") String both, @JsonProperty("value") Number value) {
        String[] split = both.split(":");
        first = split[0];
        second = split[1];
        this.value = value;
    }
}

But it I get an error:

Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of entry.JacksonObjectTest$MyClass[] out of START_OBJECT token

Makes sense to me; I'm trying to parse each field of a JSON Object into an Array of Objects, and Jackson obviously isn't too pleased about it. Neglecting the @JsonProperty("both") yields:

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid type definition for type entry.JacksonObjectTest$MyClass: Argument #0 has no property name, is not Injectable: can not use as Creator [constructor for entry.JacksonObjectTest$MyClass, annotations: {interface com.fasterxml.jackson.annotation.JsonCreator=@com.fasterxml.jackson.annotation.JsonCreator(mode=DEFAULT)}]

Which also makes sense to me; it has no clue how to parse this constructor (which is really the same problem as above; me putting the annotation in is just masking that error with a different one).

So my question is; how to I make Jackson understand what I want here?

MCVE:

public class JacksonObjectTest {
    public static void main(String[] args) throws IOException {
        String data = "{\"x:y\":1,\"x:z\":2,\"u:v\":3}";
        ObjectMapper mapper = new ObjectMapper();
        JsonNode node = mapper.readTree(data);
        MyClass[] out = mapper.readValue(node.traverse(), MyClass[].class);
        System.out.println(out);
    }

    private static class MyClass {
        String first;
        String second;
        Number value;

        @JsonCreator
        public MyClass(@JsonProperty("both") String both, @JsonProperty("value") Number value) {
            String[] split = both.split(":");
            first = split[0];
            second = split[1];
            this.value = value;
        }
    }
}

EDIT: As mentioned in the comments, I do know about the method of using a TypeReference<Map<String,Number>>. This works, but I was trying to make my parsing code as contained and generic as possible, and using this solution means I have to do further conversion post-parsing to get a MyClass[] (first parse for the Map<String,Number>, then process that for the MyClass[]). Is there a way to skip the middleman (IE: tell Jackson how to process a blob of JSON of known formatting into a data type)?

3
  • 2
    How about parsing them into map and then converting map into a list? Commented May 7, 2018 at 15:44
  • @soon That was my initial approach, and you're right it functions. Sorry, I should've stated that in the question. I was trying to make my JSON parsing code a little more generic (so that each object I wanted to parse could be extracted from the same generic parsing method), and this one was sticking out like a sore thumb in that case. I did not get that across very well with my MCVE. Commented May 7, 2018 at 15:48
  • I guess, in short, I am wondering if there is a way to tell Jackson how to convert a blob of JSON (of known formatting) into MyClass[] without having to handle all the interim data types explicitly? Commented May 7, 2018 at 15:53

3 Answers 3

2

You can use JsonAnySetter annotation which annotates method used to read all properties in object:

class MultiNamedProperties {

    private List<Property> properties = new ArrayList<>();

    @JsonAnySetter
    public void readProperty(String property, Number value) {
        String[] names = property.split(":");
        properties.add(new Property(names[0], names[1], value));
    }

    @Override
    public String toString() {
        return "MultiNamedProperties{" +
                "properties=" + properties +
                '}';
    }
}

class Property {

    private final String first;
    private final String second;
    private final Number value;

    Property(String first, String second, Number value) {
        this.first = first;
        this.second = second;
        this.value = value;
    }

    @Override
    public String toString() {
        return "MyClass{" +
                "first='" + first + '\'' +
                ", second='" + second + '\'' +
                ", value=" + value +
                '}';
    }
}

You can use it like below:

import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.ArrayList;
import java.util.List;

    public class Main {

        public static void main(String[] args) throws Exception {
            String data = "{\"x:y\":1,\"x:z\":2,\"u:v\":3}";
            ObjectMapper mapper = new ObjectMapper();
            MultiNamedProperties mnp = mapper.readValue(data, MultiNamedProperties.class);
            System.out.println(mnp);
        }
    }

Above example prints:

MultiNamedProperties{properties=[MyClass{first='x', second='y', value=1}, MyClass{first='x', second='z', value=2}, MyClass{first='u', second='v', value=3}]}

This solution needs only one annotation and two objects.

Sign up to request clarification or add additional context in comments.

Comments

1

Not sure if jackson can parse your data structure using only builtin annotations and classes. When I need to add logic while parsing json I always write own deserializer:

public void loadJsonObjectAsArray() throws IOException {
    String data = "{\"x:y\":1,\"x:z\":2,\"u:v\":3}";
    ObjectMapper mapper = new ObjectMapper();
    Wrapper wrapper = mapper.readValue(data, Wrapper.class);
    List<MyClass> out = wrapper.values;
    System.out.println(out);
}

public static class WrapperDeserializer extends StdDeserializer<Wrapper> {
    public WrapperDeserializer() {
        this(null);
    }

    public WrapperDeserializer(Class<?> vc) {
        super(vc);
    }

    @Override
    public Wrapper deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
        JsonNode node = jp.getCodec().readTree(jp);
        ObjectNode obj = (ObjectNode) node;
        List<MyClass> parsedFields = new ArrayList<>();

        obj.fields().forEachRemaining(fieldAndNode -> {
            String fieldName = fieldAndNode.getKey();
            Number value = fieldAndNode.getValue().numberValue();
            parsedFields.add(new MyClass(fieldName, value));
        });

        return new Wrapper(parsedFields);
    }
}

private static class MyClass {
    String first;
    String second;
    Number value;

    public MyClass(String both, Number value) {
        String[] split = both.split(":");
        first = split[0];
        second = split[1];
        this.value = value;
    }

    @Override
    public String toString() {
        return "MyClass{" +
                "first='" + first + '\'' +
                ", second='" + second + '\'' +
                ", value=" + value +
                '}';
    }
}

@JsonDeserialize(using = WrapperDeserializer.class)
private static class Wrapper {
    private final List<MyClass> values;

    public Wrapper(List<MyClass> values) {
        this.values = new ArrayList<>(values);
    }
}

1 Comment

This is the functionality I was looking for (specifically, @JsonDeserialize and StdDeserializer). I can apply this to my real-world problem. Thank you.
1

EDIT: The other answers provided do a better job of answering the question as a whole. Will leave this in place to provide a reference for the solution mentioned in the comments (likely easier for those who just want to quickly format data).


This is my current solution. I would note that it does not solve the problem mentioned in the edit section of the question.

The approach used is as mentioned in the comments: parse as a Map<String,Number> first, and then convert that into List<MyClass>:

public class JacksonObjectTest {
    public static void main(String[] args) throws IOException {
        String data = "{\"x:y\":1,\"x:z\":2,\"u:v\":3}";
        ObjectMapper mapper = new ObjectMapper();
        JsonNode node = mapper.readTree(data);
        // Note the difference in these two lines from the MCVE.
        Map<String,Number> interim = mapper.readValue(node.traverse(), new TypeReference<Map<String,Number>>(){});
        List<MyClass> out = interim.entrySet().stream().map(MyClass::new).collect(Collectors.toList());
        System.out.println(out);
    }

    private static class MyClass {
        String first;
        String second;
        Number value;

        public MyClass(Entry<String, Number> entry) {
            String[] split = entry.getKey().split(":");
            first = split[0];
            second = split[1];
            value = entry.getValue();
        }
    }
}

4 Comments

This is not a good way to mark right answer. This is not how SO works.
That is one way to skin the cat. Yet another would be to use "delegating" constructor, in which you take type like ObjectNode or Map as argument; Jackson binds JSON into that type, and then you can extract whatever you want from that. So instead of manually mapping to Map<String,Object>, declare @JsonCreator annotated constructor with one (and only!) argument, WITHOUT @JsonProperty -- that will then be bound from the whole matching JSON value.
@MichałZiober I disagree. This is an answer that a lot of people will likely come looking for if they come across a question titled "Jackson Parse JSON Object as Array of Objects". This is the lowest effort answer to get them up and running. I think that both you and soon have provided better answers that tailor themselves to the question asked in its entirety, and have upvoted them accordingly. But I do not feel like that detracts from the value that this simple answer would have to potential future readers, and that consideration is how StackOverflow works.
@Ironcache I understand your point of view. But in case this question will receive 20 answers and your answer will be with smallest number of upvotes nobody will see it. You can for example edit your question and add "Response section" which contains example which works. But you can leave it like it is.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.