Home > OS >  Java: ObjectMapper changes value/structure of Pojo after request/response
Java: ObjectMapper changes value/structure of Pojo after request/response

Time:10-30

I can see before the conversion of the response to my DTO it looks like this:

{
    "name": "HalloweenBundle2021",
    "pricing":
    {
        "VirtualCurrencyDto(physicalCurrency=EUR, coinId=null)":
        {
            "amount": 8.99,
            "discountAmount": 0
        },
        "VirtualCurrencyDto(physicalCurrency=USD, coinId=null)":
        {
            "amount": 9.99,
            "discountAmount": 0
        }
    }
}

which is correct.

However, after the response/actual conversion, both of the physicalCurrency fields are null, whereas the coinId fields get full map values:

{
    "name": "HalloweenBundle2021",
    "pricing":
    {
        "VirtualCurrencyDto(physicalCurrency=null, coinId=VirtualCurrencyDto(physicalCurrency=EUR, coinId=null))":
        {
            "amount": 8.99,
            "discountAmount": 0
        },
        "VirtualCurrencyDto(physicalCurrency=null, coinId=VirtualCurrencyDto(physicalCurrency=USD, coinId=null))":
        {
            "amount": 9.99,
            "discountAmount": 0
        }
    }
}

What is going on exactly?

My Pojo for the response is:

@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class FooDto implements Serializable {
  private String name;
  private Map<VirtualCurrencyDto, PriceDto> pricing = new LinkedHashMap<>();
}

and the pojo in with the issue is:

@Data
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class VirtualCurrencyDto implements Serializable {

  // both of these fields should not be present, only one or the other
  private DTOCurrency physicalCurrency; // an external enum
  private String coinId;

  public VirtualCurrencyDto(DTOCurrency physicalCurrency) {
    this.physicalCurrency = physicalCurrency;
  }

  public VirtualCurrencyDto(String coinId) {
    this.coinId = coinId;
  }
}

my objectMapper bean

  @Bean
  public ObjectMapper getObjectMapper() {
    return new ObjectMapper()
        .setSerializationInclusion(JsonInclude.Include.ALWAYS)
        .registerModule(new JsonNullableModule());
  }

And the response is being fetched via MockMvc

  public FooDto getFooDto(String name) throws Exception {
    MvcResult result =
        mockMvc.perform(
                get("/foo/"   name))
            .andExpect(status().isOk())
            .andReturn();

    return objectMapper.readValue(
        result.getResponse().getContentAsString(), // this part is fine, when I look at the string value
        FooDto.class); // issue here, upon conversion
      }

I should note that the same is true when making the request, e.g. before the request reaches my controller for a PUT, the data looks fine, but upon receiving it in the controller, the data is screwed like above (the response from GET).

CodePudding user response:

My guess is that the issue exists because you have a complex object as the key of your pricing Map. Maps in JSON are represented by a simple String as key and the value could be a complex object. This means that Jackson has to find a way to serialize your complex VirtualCurrencyDto object into a simple String. The only way to do this is via the toString() method, which is why you see "VirtualCurrencyDto(physicalCurrency=null, coinId=VirtualCurrencyDto(physicalCurrency=EUR, coinId=null))". This is wrong per se, because you should use simple objects that have a simple String representation as keys in JSON maps. But next, we will see why you are getting such weird behavior.

Jackson tries to deserialize "VirtualCurrencyDto(physicalCurrency=null, coinId=VirtualCurrencyDto(physicalCurrency=EUR, coinId=null))" into a VirtualCurrencyDto object. Since it contains only a single String property my guess is that it creates an instance of VirtualCurrencyDto with "VirtualCurrencyDto(physicalCurrency=null, coinId=VirtualCurrencyDto(physicalCurrency=EUR, coinId=null))" as the value for coinId, the only String property in VirtualCurrencyDto, hence the behaviour you are experiencing.

You have two options here:

  1. You review your model classes so that you use a simple object as the key of pricing Map. DTOCurrency would be a good candidate for example.
  2. You create a custom deserializer for VirtualCurrencyDto that is able to parse the String "VirtualCurrencyDto(physicalCurrency=null, coinId=VirtualCurrencyDto(physicalCurrency=EUR, coinId=null))" and create an instance of the class. For this you will need to extend StdDeserializer and then register it in your ObjectMapper. Something similar to the following:
SimpleModule module = new SimpleModule()
        .addDeserializer(VirtualCurrencyDto.class, new VirtualCurrencyDtoDeserializer());

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(module);

I would go with the first one.

  • Related