8

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?

Wong
  • 83
  • 1
  • 5
  • What do you mean by "many other content types, including customized types". You're working with JSON so wouldn't the content type would be `application/json`? – Zack Macomber Jul 17 '20 at 20:00
  • Do you have control over the JSON? That is, can you fix this problem at the root and have your JSON accurately reflect the data model? – dnault Jul 17 '20 at 20:09

2 Answers2

5

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.

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
-2

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.

  • Hi, I would like to know what is wrong with the answer, if the model can be changed it would solve the problem (as also commented by @dnault). But if the model can not be changed then please ignore my answer. Thanks! – Gaurav kumar Singh Jul 17 '20 at 20:33
  • Dodging the problem isn't really a valuable solution. The question is strictly about how to transform a string input into a list. Otherwise, there is no question at all. – LoganMzz Aug 24 '23 at 14:18