Wir haben einen Dienst, der derzeit JSON verbraucht. Wir wollen diesen JSON leicht umstrukturieren (eine Eigenschaft um eine Ebene nach oben verschieben), aber auch eine elegante Migration implementieren, so dass unser Dienst sowohl die alte Struktur als auch die neue Struktur verarbeiten kann. Wir verwenden Jackson für die JSON-Deserialisierung.Strukturieren Sie JSON vor dem Deserialisieren mit Jackson
Wie restrukturieren wir JSON vor der Deserialisierung mit Jackson?
Hier ist ein MCVE.
Nehmen wir unsere alten JSON sieht wie folgt aus:
{"reference": {"number" : "one", "startDate" : [2016, 11, 16], "serviceId" : "0815"}}
Wir wollen serviceId
eine Ebene nach oben:
{"reference": {"number" : "one", "startDate" : [2016, 11, 16]}, "serviceId" : "0815"}
Dies sind die Klassen, die wir von beide alt einen neuen deserialisieren möchten JSONs:
public final static class Container {
public final Reference reference;
public final String serviceId;
@JsonCreator
public Container(@JsonProperty("reference") Reference reference, @JsonProperty("serviceId") String serviceId) {
this.reference = reference;
this.serviceId = serviceId;
}
}
public final static class Reference {
public final String number;
public final LocalDate startDate;
@JsonCreator
public Reference(@JsonProperty("number") String number, @JsonProperty("startDate") LocalDate startDate) {
this.number = number;
this.startDate = startDate;
}
}
Wir auf ly wollen serviceId
in Container
, nicht in beiden Klassen.
Was ich Arbeits haben, ist die folgende Deserializer:
public static class ServiceIdMigratingContainerDeserializer extends JsonDeserializer<Container> {
private final ObjectMapper objectMapper;
{
objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
@Override
public Container deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
ObjectNode node = p.readValueAsTree();
migrate(node);
return objectMapper.treeToValue(node, Container.class);
}
private void migrate(ObjectNode containerNode) {
TreeNode referenceNode = containerNode.get("reference");
if (referenceNode != null && referenceNode.isObject()) {
TreeNode serviceIdNode = containerNode.get("serviceId");
if (serviceIdNode == null) {
TreeNode referenceServiceIdNode = referenceNode.get("serviceId");
if (referenceServiceIdNode != null && referenceServiceIdNode.isValueNode()) {
containerNode.set("serviceId", (ValueNode) referenceServiceIdNode);
}
}
}
}
}
Dieser Deserializer ruft zuerst den Baum, manipuliert sie und Deserializer es dann eine eigene Instanz von ObjectMapper
verwenden. Es funktioniert, aber wir mögen es wirklich nicht, dass wir hier eine andere Instanz von ObjectMapper
haben. Wenn wir es nicht erstellen und irgendwie die systemweite Instanz von ObjectMapper
verwenden, erhalten wir einen unendlichen Zyklus, denn wenn wir versuchen, objectMapper.treeToValue
aufzurufen, wird unser Deserializer rekursiv aufgerufen. Das funktioniert (mit einer eigenen Instanz von ObjectMapper
), aber es ist keine optimale Lösung.
Eine andere Methode, die ich versucht habe, wurde eine BeanDeserializerModifier
und eine eigene JsonDeserializer
welche „Wraps“ die Standard-Serializer mit:
public static class ServiceIdMigrationBeanDeserializerModifier extends BeanDeserializerModifier {
@Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc,
JsonDeserializer<?> defaultDeserializer) {
if (beanDesc.getBeanClass() == Container.class) {
return new ModifiedServiceIdMigratingContainerDeserializer((JsonDeserializer<Container>) defaultDeserializer);
} else {
return defaultDeserializer;
}
}
}
public static class ModifiedServiceIdMigratingContainerDeserializer extends JsonDeserializer<Container> {
private final JsonDeserializer<Container> defaultDeserializer;
public ModifiedServiceIdMigratingContainerDeserializer(JsonDeserializer<Container> defaultDeserializer) {
this.defaultDeserializer = defaultDeserializer;
}
@Override
public Container deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
ObjectNode node = p.readValueAsTree();
migrate(node);
return defaultDeserializer.deserialize(new TreeTraversingParser(node, p.getCodec()), ctxt);
}
private void migrate(ObjectNode containerNode) {
TreeNode referenceNode = containerNode.get("reference");
if (referenceNode != null && referenceNode.isObject()) {
TreeNode serviceIdNode = containerNode.get("serviceId");
if (serviceIdNode == null) {
TreeNode referenceServiceIdNode = referenceNode.get("serviceId");
if (referenceServiceIdNode != null && referenceServiceIdNode.isValueNode()) {
containerNode.set("serviceId", (ValueNode) referenceServiceIdNode);
}
}
}
}
}
„Wrapping“ eine Standard-Deserializer scheint ein besserer Ansatz zu sein, aber dies nicht gelingt mit ein NPE:
java.lang.NullPointerException
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:157)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:150)
at de.db.vz.rikernpushadapter.migration.ServiceIdMigrationTest$ModifiedServiceIdMigratingContainerDeserializer.deserialize(ServiceIdMigrationTest.java:235)
at de.db.vz.rikernpushadapter.migration.ServiceIdMigrationTest$ModifiedServiceIdMigratingContainerDeserializer.deserialize(ServiceIdMigrationTest.java:1)
at com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:1623)
at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1217)
at ...
der ganze MCVE-Code ist in dem folgenden PasteBin. Es ist ein einklassiger Testfall, der beide Ansätze demonstriert. Die migratesViaDeserializerModifierAndUnmarshalsServiceId
schlägt fehl.
So lässt mich das mit einer Frage:
Wie strukturieren wir JSON, bevor sie mit Jackson Deserialisierung?