在其他反序列化器中调用自定义 Jackson 反序列化器

Posted

技术标签:

【中文标题】在其他反序列化器中调用自定义 Jackson 反序列化器【英文标题】:Invoking custom Jackson deserializers inside of other deserializers 【发布时间】:2017-09-10 14:15:14 【问题描述】:

我正在编写一个使用 Jackson 进行序列化的 Spring Boot 应用程序(RESTful Web 服务)。我有以下数据模型,它们将在服务及其 HTTP 客户端之间来回发送(因此这些数据模型将被序列化/反序列化为 JSON):

public abstract class BaseEntity 
    @JsonIgnore
    private Long id;

    private UUID refId;

    // Getters, setters, ctors, etc.


public abstract class BaseLookup extends BaseEntity 
    private String name;

    private String label;

    private String description;

    // Getters, setters, ctors, etc.    


public class State extends BaseLookup 
    private String abbrev;    // "VT", "FL", etc.

    // Getters, setters, ctors, etc.    


public class Contact extends BaseEntity 
    private String givenName;

    private String surname;

    private State state;

    // Getters, setters, ctors, etc.    


public class Account extends BaseEntity 
    private Contact contact;

    private String code;

    // lots of other fields that will be generated server-side

    // Getters, setters, ctors, etc.

因此会有一些端点用于 CRUDding Accounts,其他端点用于 CRUDding Contacts 等。例如,AccountController 将暴露端点用于 CRUDding Account 实例:

@RestController
@RequestMapping(value = "/accounts")
public class AccountController 
    @RequestMapping(method = RequestMethod.POST)
    public void createAccount(@RequestBody Account account) 
        // Do stuff and persist the account to the DB
    

我想简化 HTTP 客户端必须制作的 JSON,以便创建新的 AccountContact 等实例。同时,我不想向客户端公开这些数据模型上的字段。 BaseEntity#id 之类的东西(这是数据库中实体的 PK)。或者例如,在State 的情况下,我只想让客户端知道(并使用)abbrev 字段等。我不希望他们看到其他 BaseLookup 字段或甚至知道他们。

因此,我的最终目标是允许客户端发布以下 JSON,并让 custom Jackson deserializer 将该 JSON 转换为 Account 实例:


  "contact" : 
    "givenName" : "Him",
    "surname" : "Himself",
    "state" : "NY"
  ,
  "code" : "12345"

所以你看,就像我上面所说的,这个 JSON 完成了几件事:

    在 POST 以创建新实例时,客户端不提供 BaseEntity#idBaseEntity#refId 对于contact.state 字段,这是一个带有多个字段(idrefIdnamelabeldescriptionabbrev)的BaseLookup,用户只需提供abbrev 字段,反序列化程序将确定客户端指的是哪个State Account 类实际上有许多其他字段是在服务器端推断/生成的;客户端无需了解它们即可创建 Account 实例 上面的 JSON 是我们使用 Jackson 的默认行为序列化 Account 得到的简化形式;这是为了让客户端的事情更容易,在服务器端更安全(不暴露 PK 等)

这里需要注意的重要一点是发送到此控制器的 contact 字段的 JSON 与将发送到 ContactController 以创建新的 Contact 的 JSON 相同实例。

问题来了:

public class AccountDeserializer extends StdDeserializer<Account> 
    public AccountDeserializer() 
        this(null);
    

    public AccountDeserializer(Class<Account> accClazz) 
        super(accClazz);
    

    @Override
    public Account deserialize(JsonParser jsonParser, DeserializationContext dCtx)
            throws IOException, JsonProcessingException 
        JsonNode jsonNode = jsonParser.codec.readTree(jsonParser)

        Contact contact = ??? // TODO: How to invoke ContactDeserializer here?

        String accountCode = node.get("code").asText();

        // Generate lots of other Account field values here...

        Account account = new Account(contact, accountCode, /* other fields here */);

        return account;
    

因为我还将有一个 ContactController(用于 CRUDding Contact 实例,而不管关联的 Account),并且因为我有类似的愿望从客户端隐藏 Contact 字段以及简化JSON 进入这个ContactController#createContact 端点,除了这个AccountDeserializer 之外,我还需要一个ContactDeserializer...

public class ContactDeserializer extends StdDeserializer<Contact> 
    // ...etc.

这个ContactDeserializer 将负责将JSON 转换为Contact 实例。但是由于Account 实例还包含Contact 实例,并且由于外部“帐户 JSON”中的“联系人 JSON”将与客户端发送到任何“联系人端点”的任何 JSON 相同,所以我会喜欢以某种方式从AccountDeserializer 内部调用ContactDeserializer

这样,当ContactController 接收“联系人 JSON”以创建新的 Contact 实例时,ContactDeserializer 将参与完成工作。 并且,如果 AccountController 接收“帐户 JSON”以创建新的 Account 实例,那么 AccountDeserializer 将参与完成这项工作... 它还使用ContactDeserialzer 来处理帐户 JSON 的内部 contact 字段的反序列化。

可以这样做吗?! 一个 Jackson 解串器可以在其中重复使用其他解串器吗?如果是这样,怎么做?如果没有,那这里的解决方案是什么?!

【问题讨论】:

为什么不在对象映射器中注册之前通过构造函数将ContactDeserializer 注入到您的AccountDeserializer 中?如果您查看 Jackson 源代码,您还会注意到在 StdDeserializer 的各种实现中都在做同样的事情。 见docs.spring.io/spring-boot/docs/1.5.2.RELEASE/reference/… 【参考方案1】:

您可以通过调用ObjectCodectreeToValue 方法来调用ContactDeserializer。如果您在ObjectMapper 上注册了ContactDeserializer,Jackson 将自动为您提取。

public class AccountDeserializer extends JsonDeserializer<Account> 

    @Override
    public Account deserialize(JsonParser p, DeserializationContext ctx) throws IOException 
        JsonNode node = p.readValueAsTree();
        JsonNode contactNode = node.get("contact");

        Contact contact = null;
        if (contactNode != null) 
            contact = p.getCodec().treeToValue(contactNode, Contact.class);
        
        return new Account(contact, /* account properties */);
    

编辑

如果您想将您的反序列化器添加到由 Spring Boot 创建的现有映射器中,您可以在您的配置类之一中自动装配它并根据需要进行自定义。

@Configuration
public class ObjectMapperConfiguration 

    @Autowired
    public void configureObjectMapper(ObjectMapper mapper) 
        SimpleModule module = new SimpleModule()
                .addDeserializer(Account.class, new AccountDeserializer())
                .addDeserializer(Contact.class, new ContactDeserializer());

        mapper.registerModule(module);
    

【讨论】:

有趣的想法,感谢@Edd (+1) - 但请记住这是一个 Spring Boot 应用程序,关于我如何在 Spring 的 ObjectMapper 上“注册”自定义序列化器/反序列化器的任何想法在引擎盖下提供?再次感谢! @smeeb 我更新了我的答案,包括一个如何在现有映射器上注册反序列化器的示例。

以上是关于在其他反序列化器中调用自定义 Jackson 反序列化器的主要内容,如果未能解决你的问题,请参考以下文章

Jackson 将 YAML 文件反序列化为 Map(没有自定义反序列化器)

Spring Boot 1.4 自定义内部 Jackson 反序列化

当值为“null”时,Jackson 忽略自定义字段反序列化器

Spring Boot 自定义Jackson ObjectMapper

Jackson 使用自定义日期格式错误地反序列化 Joda 日期

为 Jackson 自定义反序列化程序抛出带有 HTTP 状态代码的自定义异常