在其他反序列化器中调用自定义 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,以便创建新的 Account
、Contact
等实例。同时,我不想向客户端公开这些数据模型上的字段。 BaseEntity#id
之类的东西(这是数据库中实体的 PK)。或者例如,在State
的情况下,我只想让客户端知道(并使用)abbrev
字段等。我不希望他们看到其他 BaseLookup
字段或甚至知道他们。
因此,我的最终目标是允许客户端发布以下 JSON,并让 custom Jackson deserializer 将该 JSON 转换为 Account
实例:
"contact" :
"givenName" : "Him",
"surname" : "Himself",
"state" : "NY"
,
"code" : "12345"
所以你看,就像我上面所说的,这个 JSON 完成了几件事:
-
在 POST 以创建新实例时,客户端不提供
BaseEntity#id
或 BaseEntity#refId
对于contact.state
字段,这是一个带有多个字段(id
、refId
、name
、label
、description
、abbrev
)的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】:
您可以通过调用ObjectCodec
的treeToValue
方法来调用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