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
).
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:
pricing
Map. DTOCurrency
would be a good candidate for example.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.