Java: ObjectMapper 在 request/response 之后改变了 Pojo 的 value/structure

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

我可以看到在将响应转换为我的 DTO 之前它看起来像这样:

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

这是正确的。

但是,在 response/actual 转换之后,两个 physicalCurrency 字段都为空,而 coinId 字段获得完整的映射值:

{
    "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
        }
    }
}

到底是怎么回事?

我的回复 Pojo 是:

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

问题所在的 pojo 是:

@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;
  }
}

我的 objectMapper bean

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

并且正在通过 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
      }

我应该注意,在提出请求时也是如此,例如在 PUT 的请求到达我的控制器之前,数据看起来很好,但是在控制器中接收到它后,数据像上面那样搞砸了(来自 GET 的响应)。

我的猜测是问题存在是因为您有一个复杂的对象作为 pricing 地图的键。 JSON 中的映射由一个简单的 String 表示为键,值可以是一个复杂的对象。这意味着 Jackson 必须找到一种方法将复杂的 VirtualCurrencyDto 对象序列化为简单的 String。唯一的方法是通过 toString() 方法,这就是您看到 "VirtualCurrencyDto(physicalCurrency=null, coinId=VirtualCurrencyDto(physicalCurrency=EUR, coinId=null))" 的原因。这是错误的 本身 ,因为您应该使用具有简单 String 表示的简单对象作为 JSON 映射中的键。但接下来,我们将了解为什么您会出现这种奇怪的行为。

Jackson 尝试将 "VirtualCurrencyDto(physicalCurrency=null, coinId=VirtualCurrencyDto(physicalCurrency=EUR, coinId=null))" 反序列化为 VirtualCurrencyDto 对象。因为它只包含一个 String 属性 我的猜测是它创建了一个 VirtualCurrencyDto 的实例,其中 "VirtualCurrencyDto(physicalCurrency=null, coinId=VirtualCurrencyDto(physicalCurrency=EUR, coinId=null))" 作为 coinId 的值,唯一的 String 属性 在 VirtualCurrencyDto 中,因此您遇到的行为。

这里有两个选择:

  1. 您检查了您的模型 classes,以便使用一个简单的对象作为 pricing 地图的键。例如,DTOCurrency 将是一个很好的候选人。
  2. 您为 VirtualCurrencyDto 创建了一个自定义反序列化器,它能够解析 String "VirtualCurrencyDto(physicalCurrency=null, coinId=VirtualCurrencyDto(physicalCurrency=EUR, coinId=null))" 并创建 class 的实例。为此,您需要扩展 StdDeserializer,然后在您的 ObjectMapper 中注册它。类似于以下内容:
SimpleModule module = new SimpleModule()
        .addDeserializer(VirtualCurrencyDto.class, new VirtualCurrencyDtoDeserializer());

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

我会选择第一个。