skip to Main Content

I have an object like this:

class Test {
    List<Integer> a;
}

I want both the following json to be parsed correctly:

{ "a" : "[1, 2, 3]" }
{ "a" : [1, 2, 3] }

When I try to deserialize it in Jackson with the data type as follows, it throws the following exception:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot deserialize value of type `java.util.ArrayList<java.lang.Integer>` from String value (token `JsonToken.VALUE_STRING`)

How can I create a custom deserializer for this case? I’ve already tried creating one like the following, and it doesn’t work and wasn’t called during deserialization, probably because of generic type erasure.

val jackson = ObjectMapper().apply {
    registerModule(SimpleModule().addDeserializer(ArrayList::class.java, object : JsonDeserializer<ArrayList<Integer>>() {
        override fun deserialize(parser: JsonParser, context: DeserializationContext) =
            parser.text.trim('[', ']').split(',').map { it.toInt() } as ArrayList<Integer>
    }))
}

2

Answers


  1. Well, I found a solution. If you are ready to use a JsonNode, this is possible. Although this code does not involve your Test class, maybe you can work on it yourself.
    Try below mentioned code:

    public static void main(String[] args) {
            String enteredJson = "{ "a" : "[1, 2, 3]" }";
    //        String json1 = "{ "a" : [1, 2, 3] }";
    
            try {
                ObjectMapper objectMapper = new ObjectMapper();
    
                JsonNode jsonNode1 = objectMapper.readTree(enteredJson);
    
                List<Integer> list1;
                 if (jsonNode1.has("a")) {
                    if (jsonNode1.get("a").isTextual()) {
                        String innerJson1 = jsonNode1.get("a").asText();
                        list1 = objectMapper.readValue(innerJson1, new ArrayList<>().getClass());
                        System.out.println("List from String: " + list1);
                    } else if (jsonNode1.get("a").isArray()) {
                        list1 = objectMapper.readValue(jsonNode1.get("a").traverse(), new ArrayList<>().getClass());
                        System.out.println("List from List: " + list1);
                    }
                }
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        
    

    You can comment/uncomment the enteredJson and check.

    EDIT

    Here is another solution that involves your Test class.

    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    /**
     *
     * @author asgar
     */
    public class Test {
    
        List<Integer> a;
    
        public List<Integer> getA() {
            return a;
        }
    
        public void setA(Object a) {
            try {
                List<Integer> list1 = null;
                if (a instanceof String) {
                    String bracketsRemoved = a.toString().replace("[", "").replace("]", "");
                    List<String> list = new ArrayList<>(Arrays.asList(bracketsRemoved.split(", ")));
                    list1 = list.stream().map(e -> Integer.parseInt(e)).collect(Collectors.toList());
                } else if (a instanceof ArrayList) {
                    list1 = (List) a;
                }
                this.a = list1;
            } catch (Exception e) {
                this.a = null;
            }
        }
    }
    

    And your main class:

    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    /**
     *
     * @author asgar
     */
    public class MainClass {
    
        public static void main(String[] args) {
            try {
                String value = "{ "a" : "[1, 2, 3]" }";
                ObjectMapper mapper = new ObjectMapper();
    
                Test test = mapper.readValue(value, Test.class);
                System.out.println("MY LIST " + test.a);
            } catch (JsonProcessingException pre) {
                pre.printStackTrace();
            }
        }
    
    }    
    

    For this solution I have passed Object into the set method, you can figure out the rest.

    Login or Signup to reply.
  2. I want to provide a solution with a different approach using the class StdDeserializer, as I think it would centralize better the unmarshalling logic in a single point, making it more compact, flexible and readable.

    Firstly, we need to separate the two cases:

    Elements as an array

    1. Create an Iterable from an Iterator of the node’s elements.
    2. Use the StreamSupport class and the Iterable.spliterator() method to stream the elements.
    3. Map each element to an int, and are automatically boxed to Integer as we have a generic Stream<T> and not an IntStream.
    4. Collect all elements into a List.

    Elements as a String

    1. Retrieve the array as a String with rootNode.get("a").asText().
    2. Remove the square brackets with a regex.
    3. Split the elements with a comma.
    4. Create a generic Stream<T> with the elements.
    5. Map each element to an Integer (and previously removing any white space).
    6. Collect all elements into a List<Integer>.

    Implementation

    MyDeserializer.java

    public class MyDeserializer extends StdDeserializer <Test> {
        public MyDeserializer() {
            this(null);
        }
    
        public MyDeserializer(Class < ? > vc) {
            super(vc);
        }
    
        @Override
        public Test deserialize(JsonParser p, DeserializationContext ctxt) throws IOException,
        JacksonException {
            JsonNode rootNode = p.getCodec().readTree(p);
    
            if (rootNode.get("a").isArray()) {
                //1. Creating an Iterable from an Iterator of the node's elements
                Iterable < JsonNode > iterable = () - > rootNode.get("a").elements();
    
                //2. Using the StreamSupport class and Iterable.spliterator method to stream the elements
                //3. Mapping each element to an int and boxing them to Integer as we have a generic Stream<T> and not an IntStream
                //4. Collecting all elements into a List<Integer>
                return new Test(StreamSupport.stream(iterable.spliterator(), false).map(node - > node.asInt()).collect(Collectors.toList()));
            }
    
            if (rootNode.get("a").isTextual()) {
                //1. Retrieving the array as a String with rootNode.get("a").asText()
                //2. Removing the square brackets with a regex
                //3. Splitting the elements with a comma
                String[] elements = rootNode.get("a").asText().replaceAll("[\[\]]", "").split(",");
    
                //4. Creating a generic Stream<T> with the elements
                //5. Mapping each element to an Integer (and previously removing any white space)
                //6. Collecting all elements into a List<Integer>
                return new Test(Arrays.stream(elements).map(s - > Integer.parseInt(s.trim())).collect(Collectors.toList()));
            }
    
            return null;
        }
    }
    

    Test.java

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @JsonDeserialize(using = MyDeserializer.class)
    public class Test {
        List<Integer> a;
    }
    

    Live Demo

    Here is also a live demo at oneCompiler.com

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search