Skip to content Skip to sidebar Skip to footer

Parsing JSON Array With Numbers As Keys Using Jackson?

How to parse following kind of JSON Array using Jackson with preserving order of the content: { '1': { 'title': 'ABC', 'category': 'Video', }, '2': { 'title': 'DE

Solution 1:

One simple solution: rather than deserializing it directly as an array/list, deserialize it to a SortedMap<Integer, Value> and then just call values() on that to get the values in order. A bit messy, since it exposes details of the JSON handling in your model object, but this is the least work to implement.

@Test
public void deserialize_object_keyed_on_numbers_as_sorted_map() throws Exception {
    ObjectMapper mapper = new ObjectMapper();
    SortedMap<Integer, Value> container = mapper
            .reader(new TypeReference<SortedMap<Integer, Value>>() {})
            .with(JsonParser.Feature.ALLOW_SINGLE_QUOTES)
            .with(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
            .readValue(
                    "{ 1: { title: 'ABC', category: 'Video' }, 2: { title: 'DEF', category: 'Video' }, 3: { title: 'XYZ', category: 'Video' } }");
    assertThat(container.values(),
            contains(new Value("ABC", "Video"), new Value("DEF", "Video"), new Value("XYZ", "Video")));
}


public static final class Value {
    public final String title;
    public final String category;

    @JsonCreator
    public Value(@JsonProperty("title") String title, @JsonProperty("category") String category) {
        this.title = title;
        this.category = category;
    }
}

But if you want to just have a Collection<Value> in your model, and hide this detail away, you can create a custom deserializer to do that. Note that you need to implement "contextualisation" for the deserializer: it will need to be aware of what the type of the objects in your collection are. (Although you could hardcode this if you only have one case of it, I guess, but where's the fun in that?)

@Test
public void deserialize_object_keyed_on_numbers_as_ordered_collection() throws Exception {
    ObjectMapper mapper = new ObjectMapper();
    CollectionContainer container = mapper
            .reader(CollectionContainer.class)
            .with(JsonParser.Feature.ALLOW_SINGLE_QUOTES)
            .with(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
            .readValue(
                    "{ values: { 1: { title: 'ABC', category: 'Video' }, 2: { title: 'DEF', category: 'Video' }, 3: { title: 'XYZ', category: 'Video' } } }");
    assertThat(
            container,
            equalTo(new CollectionContainer(ImmutableList.of(new Value("ABC", "Video"), new Value("DEF", "Video"),
                    new Value("XYZ", "Video")))));
}


public static final class CollectionContainer {
    @JsonDeserialize(using = CustomCollectionDeserializer.class)
    public final Collection<Value> values;

    @JsonCreator
    public CollectionContainer(@JsonProperty("values") Collection<Value> values) {
        this.values = ImmutableList.copyOf(values);
    }
}

(note definitions of hashCode(), equals(x) etc. are all omitted for readability)

And finally here comes the deserializer implementation:

public static final class CustomCollectionDeserializer extends StdDeserializer<Collection<?>> implements
        ContextualDeserializer {
    private JsonDeserializer<Object> contentDeser;

    public CustomCollectionDeserializer() {
        super(Collection.class);
    }

    public CustomCollectionDeserializer(JavaType collectionType, JsonDeserializer<Object> contentDeser) {
        super(collectionType);
        this.contentDeser = contentDeser;
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
            throws JsonMappingException {
        if (!property.getType().isCollectionLikeType()) throw ctxt
                .mappingException("Can only be contextualised for collection-like types (was: "
                        + property.getType() + ")");
        JavaType contentType = property.getType().getContentType();
        return new CustomCollectionDeserializer(property.getType(), ctxt.findContextualValueDeserializer(
                contentType, property));
    }

    @Override
    public Collection<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException,
            JsonProcessingException {
        if (contentDeser == null) throw ctxt.mappingException("Need context to produce elements of collection");
        SortedMap<Integer, Object> values = new TreeMap<>();
        for (JsonToken t = p.nextToken(); t != JsonToken.END_OBJECT; t = p.nextToken()) {
            if (t != JsonToken.FIELD_NAME) throw ctxt.wrongTokenException(p, JsonToken.FIELD_NAME,
                    "Expected index field");
            Integer index = Integer.valueOf(p.getText());
            p.nextToken();
            Object value = contentDeser.deserialize(p, ctxt);
            values.put(index, value);
        }
        return values.values();
    }
}

This covers at least this simple case: things like the contents of the collection being polymorphic types may require more handling: see the source of Jackson's own CollectionDeserializer.

Also, you could use UntypedObjectDeserializer as a default instead of choking if no context is given.

Finally, if you want the deserializer to return a List with the indices preserved, you can modify the above and just insert a bit of post-processing of the TreeMap:

        int capacity = values.lastKey() + 1;
        Object[] objects = new Object[capacity];
        values.forEach((key, value) -> objects[key] = value);
        return Arrays.asList(objects);

Post a Comment for "Parsing JSON Array With Numbers As Keys Using Jackson?"