Search code examples
javajacksonjackson-databind

How to Deserialize a Map<String,Object> correctly in Jackson


I have the following POJO that can be serialized into bytes or json.

public final class Message {

  private final Data data;
  private final Request request;
  private final Response response;

  public Message() {
    this.data = new Data();
    this.request = new Request();
    this.response = new Response();
  }

  public Data getData() {
    return data;
  }

  public Request getRequest() {
    return request;
  }

  public Response getResponse() {
    return response;
  }

  public Object query(String pointer) {
    return toJson().query(pointer);
  }

  public byte[] toBytes() {
    try {
      return new ObjectMapper(new MessagePackFactory()).writeValueAsBytes(this);
    } catch (JsonProcessingException ex) {
      throw new MessageException(ex);
    }
  }

  public JSONObject toJson() {
    try {
      return new JSONObject(new ObjectMapper().writeValueAsString(this));
    } catch (JsonProcessingException ex) {
      throw new MessageException(ex);
    }
  }

  @Override
  public String toString() {
    try {
      return toString(0);
    } catch (MessageException ex) {
      throw new MessageException(ex);
    }
  }

  public String toString(int indent) {
    try {
      return toJson().toString(indent);
    } catch (MessageException ex) {
      throw new MessageException(ex);
    }
  }
}

Reference Classes:

public class Data {

  private final Map<String, Map<String, Object>> dataMap;

  public Data() {
    this.dataMap = new HashMap();
  }

  public Data addToSet(String name, String key, Object value) {
    Map<String, Object> map = dataMap.get(name);
    if (map == null) {
      map = new HashMap();
    }
    map.put(key, value);
    dataMap.put(name, map);
    return this;
  }

  public Map<String, Map<String, Object>> getSets() {
    return dataMap;
  }

  public Data updateSet(String name, String key, Object value) {
    return Data.this.addToSet(name, key, value);
  }

  public Data removeFromSet(String name, String key) {
    Map<String, Object> map = dataMap.get(name);
    if (map == null) {
      throw new MessageException("No such property '" + key + "' for set '" + name + "'");
    }
    map.remove(key);
    return this;
  }

  public Map<String, Object> getSet(String name) {
    return dataMap.get(name);
  }
}
public class Request {

  private String method;
  private String resource;
  private final Map<String, Object> body;
  private final Map<String, String> headers;
  private final Map<String, String[]> parameters;

  public Request() {
    this.body = new HashMap();
    this.headers = new HashMap();
    this.parameters = new HashMap();
  }

  public String getMethod() {
    return Objects.toString(method, "");
  }

  public String getResource() {
    return Objects.toString(resource, "");
  }

  public Map<String, Object> getBody() {
    return body;
  }

  public Map<String, String> getHeaders() {
    return headers;
  }

  public Map<String, String[]> getParameters() {
    return parameters;
  }

  public String getHeader(String name) {
    return headers.get(name);
  }

  public Request setBody(String payload) {
    try {
      this.body.putAll(new ObjectMapper().readValue(payload, new TypeReference<Map<String, Object>>() {
      }));
      return this;
    } catch (JsonProcessingException ex) {
      throw new MessageException(ex);
    }
  }

  public Request setMethod(String name) {
    this.method = name;
    return this;
  }

  public Request setResource(String name) {
    this.resource = name;
    return this;
  }

  public Request setHeaders(Map<String, String> headers) {
    this.headers.putAll(headers);
    return this;
  }

  public Request setParameters(Map<String, String[]> parameters) {
    this.parameters.putAll(parameters);
    return this;
  }
}
public class Response {

  private String code;
  private String data;
  private String messageId;
  private String timestamp;
  private String description;

  public Response() {
  }

  public String getCode() {
    return Objects.toString(code, "");
  }

  public String getData() {
    return Objects.toString(data, "");
  }

  public String getMessageId() {
    return Objects.toString(messageId, "");
  }

  public String getTimestamp() {
    return Objects.toString(timestamp, "");
  }

  public String getDescription() {
    return Objects.toString(description, "");
  }

  public Response setCode(String code) {
    this.code = code;
    return this;
  }

  public Response setData(String data) {
    this.data = data;
    return this;
  }

  public Response setMessageId(String messageId) {
    this.messageId = messageId;
    return this;
  }

  public Response setTimestamp(String timestamp) {
    this.timestamp = timestamp;
    return this;
  }

  public Response setDescription(String description) {
    this.description = description;
    return this;
  }
}

When serializing to json I get a valid string

{
    "request": {
        "headers": {},
        "method": "",
        "resource": "",
        "body": {
            "whatsapp": {
                "conversationId": "39f09c41-1bd3-4e81-b829-babed3747d4b",
                "name": "Dave",
                "source": "+123456789098"
            },
            "payment": {
                "product": "chocolate",
                "amount": 1,
                "method": "cashapp",
                "msisdn": "123456789098",
                "entity": "The Fudge Shop"
            }
        },
        "parameters": {}
    },
    "data": {
        "sets": {
            "whatsapp": {
                "provider": "clickatell",
                "name": "Dave",
                "destination": "123456789098",
                "source": "123456789098",
                "message": "Your payment of $1.00 received, your receipt.no is QWJ124XPA9."
            },
            "cashapp": {
                "amount": 1,
                "receiptNo": "QWJ124XPA9",
                "name": "Dave Chapelle",
                "msisdn": "123456789098"
            }
        }
    },
    "response": {
        "code": "202",
        "data": "",
        "messageId": "20210623160202a647d32ee9ae477f9c90d8b1fbfd763a",
        "description": "Processing Request",
        "timestamp": "2021-06-23 16:02:02.408"
    }
}

When I attempt to deserialize the json back to a pojo

Message output = new ObjectMapper().readValue(json.toString(), Message.class);

I get the error :

com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.lang.String` out of START_OBJECT token

The error seems to be generated from the Request class when attempting to deserialize the Map<String, Object> body:

How may I deserialize the Map correctly?


Solution

  • The solution that worked for me was using custom deserialization, @JsonDeserialize annotation & JsonDeserializer interface, in order to achieve the desired results.

    Below is the solution:

    public class Request {
    
      private String method;
      private String resource;
      @JsonDeserialize(using = BodyDeserializer.class)
      private final Map<String, Object> body;
      private final Map<String, String> headers;
      private final Map<String, String[]> parameters;
    
      public Request() {
        this.body = new HashMap();
        this.headers = new HashMap();
        this.parameters = new HashMap();
      }
    
      public String getMethod() {
        return method;
      }
    
      public String getResource() {
        return resource;
      }
    
      public Map<String, Object> getBody() {
        return body;
      }
    
      public Map<String, String> getHeaders() {
        return headers;
      }
    
      public Map<String, String[]> getParameters() {
        return parameters;
      }
    
      public String getHeader(String name) {
        return headers.get(name);
      }
    
      public Request setBody(Map<String, Object> body) {
        this.body.putAll(body);
        return this;
      }
    
      public Request setMethod(String name) {
        this.method = name;
        return this;
      }
    
      public Request setResource(String name) {
        this.resource = name;
        return this;
      }
    
      public Request setHeaders(Map<String, String> headers) {
        this.headers.putAll(headers);
        return this;
      }
    
      public Request setParameters(Map<String, String[]> parameters) {
        this.parameters.putAll(parameters);
        return this;
      }
    
      private static class BodyDeserializer extends JsonDeserializer<Map<String, Object>> {
    
        @Override
        public Map<String, Object> deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
          JsonDeserializer<Object> deserializer = dc.findRootValueDeserializer(dc.constructType(Map.class));
          Map<String, Object> map = (Map<String, Object>) deserializer.deserialize(jp, dc);
          return map;
        }
      }
    }