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:
- 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. - You create a custom deserializer for
VirtualCurrencyDto
that is able to parse theString
"VirtualCurrencyDto(physicalCurrency=null, coinId=VirtualCurrencyDto(physicalCurrency=EUR, coinId=null))"
and create an instance of the class. For this you will need to extendStdDeserializer
and then register it in yourObjectMapper
. 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.