26

I'm trying to consume a service that gives me an entity with a field that it's an array.

{
  "id": "23233",
  "items": [
    {
      "name": "item 1"
    },
    {
      "name": "item 2"
    }
  ]
}

But when the array contains a single item, the item itself is returned, instead of an array of one element.

{
  "id": "43567",
  "items": {
      "name": "item only"
    }
}

In this case, Jackson fails to convert to my Java object.

public class ResponseItem {

   private String id;
   private List<Item> items;

   //Getters and setters...
}

Is there an straightforward solution for it?

chrylis -cautiouslyoptimistic-
  • 75,269
  • 21
  • 115
  • 152
Javier Alvarez
  • 1,409
  • 2
  • 15
  • 33

4 Answers4

45

You are not the first to ask for this problem. It seems pretty old.

After looking at this problem, you can use the DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY :

Look at the documentation : http://fasterxml.github.io/jackson-databind/javadoc/2.5/com/fasterxml/jackson/databind/DeserializationFeature.html#ACCEPT_SINGLE_VALUE_AS_ARRAY

You need to add this jackson feature to your object mapper.

I hope it will help you.

Thomas Betous
  • 4,633
  • 2
  • 24
  • 45
20

I think the anotation suits the code better.

@JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
private List<Item> items;
Higarian
  • 533
  • 5
  • 11
  • 1
    In case this is not working and you're using lombok: You have to make this field non-`final` and use `@NoArgsContructor`. It doesn't work for `final` fields. – lilalinux Jun 04 '21 at 08:32
  • 1
    Unfortunately, it doesn't seem to work with `@JsonValue`. – Florens Dec 02 '21 at 09:14
2

A custom JsonDeserializer can be used to solve this.

E.g.

class CustomDeserializer extends JsonDeserializer<List<Item>> {

    @Override
    public List<Item> deserialize(JsonParser jsonParser, DeserializationContext context)
            throws IOException, JsonProcessingException {

        JsonNode node = jsonParser.readValueAsTree();

        List<Item> items = new ArrayList<>();
        ObjectMapper mapper = new ObjectMapper();

        if (node.size() == 1) {
            Item item = mapper.readValue(node.toString(), Item.class);
            items.add(item);

        } else {
            for (int i = 0; i < node.size(); i++) {
                Item item = mapper.readValue(node.get(i).toString(), Item.class);
                items.add(item);
            }
        }

        return items;
    }

}

you need to tell jackson to use this to de-serialize items, like:

@JsonDeserialize(using = CustomDeserializer.class)
private List<Item> items;

After that it will work. Happy coding :)

Sachin Gupta
  • 7,805
  • 4
  • 30
  • 45
  • 1
    this is bad. won't work with deep structures where you may want single values as arrays on multiple levels. Using a separate mapper to deserialize the whole List branch in the default manner. Additional mapper gets instantiated with every List deserialization. – Radu Simionescu Jun 26 '17 at 08:18
1

@JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) does not seem work with @JsonValue.

Meaning it won't work if you want to deserialize either of these as a list:

{
  "key": true
}
[
  {
    "key": true
  },
  {
    "key": false
  }
]

As a solution it's possible to create a custom deserializer like this:

@JsonDeserialize(using = CustomDeserializer.class)
public record ListHolder(List<Item> items) {}

public class CustomDeserializer extends JsonDeserializer<ListHolder> {

    @Override
    public ListHolder deserialize(JsonParser parser, DeserializationContext context) throws IOException {
        List<Item> items;

        if (parser.isExpectedStartArrayToken()) {
            items = parser.readValueAs(new TypeReference<>() {});
        } else {
            items = List.of(parser.readValueAs(Item.class));
        }

        return new ListHolder(items);
    }
}
Florens
  • 91
  • 12