How to deserialize a string separated by comma to list with Jackson commonly?

Viewed 3068

I have a json like:

{
  "names": "John, Tom",
  "values": "8, 9",
  "statuses": "yes, no"
}

and want to deserialize to:

class Bean {
  private List<String> names;
  private List<Integer> values;
  private List<StatusEnum> statuses;
}

I know that implementing StringToStringListDeserializer, StringToIntegerListDeserializer, and StringToStatusEnumListDeserializer separately is practicable. But there are many other content types, including customized types. I tried:

public class StringToListDeserializer<T> extends JsonDeserializer<List<T>> implements ContextualDeserializer
    public List<T> deserialize(JsonParser p, DeserializationContext context) throws IOException {
        JavaType javaType = property.getType();
        if (p.hasToken(JsonToken.VALUE_STRING)) {
            String text = p.getText();
            if (StringUtils.isBlank(text)) {
                return null;
            }
            List<T> list = new LinkedList<>();
            JavaType contentType = javaType.getContentType();
            JsonDeserializer<Object> deserializer = context.findNonContextualValueDeserializer(contentType);
            for (String s : text.split(DELIMITER)) {

                // todo how to deserialize the string to a known type?

            }
            return list;
        }
        return context.readValue(p, javaType);
    }

and i don't know how to deserialize the string to the known content type. Is there any way to implement a universal deserializer?

2 Answers

To avoid manual deserialisation and handling all possible types we can use a fact that all items on the list are also JSON elements when we wrap them with a quote (") char.

So, we can convert John, Tom to a "John", "Tom", 8, 9 to "8", "9" and so on.

We can use default Jackson behaviour which allows to handle unexpected tokens. In our case whenever: STRING token appears when JSON ARRAY was expected. To handle these cases we can use com.fasterxml.jackson.databind.deser.DeserializationProblemHandler class. It could look like below:

class ComaSeparatedValuesDeserializationProblemHandler extends DeserializationProblemHandler {

    @Override
    public Object handleUnexpectedToken(DeserializationContext ctxt, JavaType targetType, JsonToken token, JsonParser parser, String failureMsg) throws IOException {
        if (token == JsonToken.VALUE_STRING && targetType.isCollectionLikeType()) {
            return deserializeAsList(targetType, parser);
        }
        return super.handleUnexpectedToken(ctxt, targetType, token, parser, failureMsg);
    }

    private Object deserializeAsList(JavaType listType, JsonParser parser) throws IOException {
        String[] values = readValues(parser);

        ObjectMapper mapper = (ObjectMapper) parser.getCodec();
        JavaType itemType = listType.getContentType();

        List<Object> result = new ArrayList<>();
        for (String value : values) {
            result.add(convertToItemType(mapper, itemType, value));
        }

        return result;
    }

    private Object convertToItemType(ObjectMapper mapper, JavaType contentType, String value) throws IOException {
        final String json = "\"" + value.trim() + "\"";

        return mapper.readValue(json, contentType);
    }

    private String[] readValues(JsonParser p) throws IOException {
        final String text = p.getText();

        return text.split(",");
    }
}

Example usage:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.google.common.base.Joiner;
import lombok.Data;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class ConvertStringToCollectionApp {
    public static void main(String[] args) throws IOException {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        ObjectMapper mapper = JsonMapper.builder()
                .addHandler(new ComaSeparatedValuesDeserializationProblemHandler())
                .build();

        Bean bean = mapper.readValue(jsonFile, Bean.class);

        print(bean.getNames());
        print(bean.getValues());
        print(bean.getStatuses());
    }

    private static void print(List<?> values) {
        values.stream().findFirst().ifPresent(value -> System.out.print(value.getClass().getSimpleName() + "s: "));
        System.out.println(Joiner.on(", ").join(values));
    }
}

@Data
class Bean {
    private List<String> names;
    private List<Integer> values;
    private List<StatusEnum> statuses;
}

enum StatusEnum {
    yes, no
}

Above app for your JSON payload prints:

Strings: John, Tom
Integers: 8, 9
StatusEnums: yes, no

I used Lombok and Guava libraries just to make it simple and short but they are not mandatory to make it work.

Your Bean doesn't correctly represents the JSON. The correct version should look something like below

class Bean {
  private String names;
  private Integer values;
  private String statuses;
}

And you can use Object Mapper

ObjectMapper objectMapper = new ObjectMapper();
Bean bean = objectMapper.readValue(json, Bean.class);  

Finally, you can break down your Bean object to list of names, values and status for your further usages.

Related