Si j'ai une structure de classe comme ça:
public abstract class Parent {
private Long id;
...
}
public class SubClassA extends Parent {
private String stringA;
private Integer intA;
...
}
public class SubClassB extends Parent {
private String stringB;
private Integer intB;
...
}
Existe-t-il une autre façon de désérialiser différents alors @JsonTypeInfo
? En utilisant cette annotation sur ma classe parent:
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "objectType")
Je préfère ne pas avoir à forcer les clients de mon API à inclure "objectType": "SubClassA"
pour désérialiser une sous-classe Parent
.
À la place d'utiliser @JsonTypeInfo
, Jackson fournit-il un moyen d'annoter une sous-classe et de la distinguer des autres sous-classes via une propriété unique? Dans mon exemple ci-dessus, ce serait quelque chose comme, "Si un objet JSON a "stringA": ...
le désérialiser comme SubClassA
, s'il a "stringB": ...
le désérialiser comme SubClassB
".
Cela ressemble à quelque chose @JsonTypeInfo
et @JsonSubTypes
devrait être utilisé, mais j'ai parcouru les documents et aucune des propriétés qui peuvent être fournies ne semble correspondre à ce que vous décrivez.
Vous pouvez écrire un désérialiseur personnalisé qui utilise @JsonSubTypes
'"nom" et "valeur" propriétés d'une manière non standard pour accomplir ce que vous voulez. Le désérialiseur et @JsonSubTypes
serait fourni sur votre classe de base et le désérialiseur utiliserait les valeurs "nom" pour vérifier la présence d'une propriété et si elle existe, puis désérialiser le JSON dans la classe fournie dans la propriété "valeur". Vos cours ressembleraient alors à ceci:
@JsonDeserialize(using = PropertyPresentDeserializer.class)
@JsonSubTypes({
@Type(name = "stringA", value = SubClassA.class),
@Type(name = "stringB", value = SubClassB.class)
})
public abstract class Parent {
private Long id;
...
}
public class SubClassA extends Parent {
private String stringA;
private Integer intA;
...
}
public class SubClassB extends Parent {
private String stringB;
private Integer intB;
...
}
Voici une solution que j'ai trouvée qui s'étend un peu sur celle d'Erik Gillespie. Il fait exactement ce que vous avez demandé et cela a fonctionné pour moi.
Utiliser Jackson 2.9
@JsonDeserialize(using = CustomDeserializer.class)
public abstract class BaseClass {
private String commonProp;
}
// Important to override the base class' usage of CustomDeserializer which produces an infinite loop
@JsonDeserialize(using = JsonDeserializer.None.class)
public class ClassA extends BaseClass {
private String classAProp;
}
@JsonDeserialize(using = JsonDeserializer.None.class)
public class ClassB extends BaseClass {
private String classBProp;
}
public class CustomDeserializer extends StdDeserializer<BaseClass> {
protected CustomDeserializer() {
super(BaseClass.class);
}
@Override
public BaseClass deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
TreeNode node = p.readValueAsTree();
// Select the concrete class based on the existence of a property
if (node.get("classAProp") != null) {
return p.getCodec().treeToValue(node, ClassA.class);
}
return p.getCodec().treeToValue(node, ClassB.class);
}
}
// Example usage
String json = ...
ObjectMapper mapper = ...
BaseClass instance = mapper.readValue(json, BaseClass.class);
Si vous voulez devenir plus amateur, vous pouvez développer CustomDeserializer
pour inclure un Map<String, Class<?>>
qui mappe un nom de propriété qui, lorsqu'il est présent, mappe vers une classe spécifique. Une telle approche est présentée dans ce article .
Au fait, il y a un problème de github qui le demande ici: https://github.com/FasterXML/jackson-databind/issues/1627
Non. Une telle fonctionnalité a été demandée - elle pourrait être appelée "inférence de type", ou "type implicite" - mais personne n'a encore proposé une proposition générale viable sur la manière dont cela devrait fonctionner. Il est facile de penser à des moyens de prendre en charge des solutions spécifiques à des cas spécifiques, mais trouver une solution générale est plus difficile.
Comme d'autres l'ont souligné, il n'y a pas de consensus sur comment cela devrait fonctionner pour qu'il ne soit pas implémenté .
Si vous avez des classes Foo, Bar et leur solution parent FooBar semble assez évidente lorsque vous avez des JSON comme:
{
"foo":<value>
}
ou
{
"bar":<value>
}
mais il n'y a pas de réponse commune à ce qui se passe lorsque vous obtenez
{
"foo":<value>,
"bar":<value>
}
À première vue, le dernier exemple semble être un cas évident de 400 Bad Request, mais il existe de nombreuses approches différentes dans la pratique:
Ma solution actuelle qui fonctionne dans la plupart des cas et essaie de tirer le meilleur parti possible de l'infrastructure Jackson existante (vous n'avez besoin que d'un désérialiseur par hiérarchie):
public class PresentPropertyPolymorphicDeserializer<T> extends StdDeserializer<T> {
private final Map<String, Class<?>> propertyNameToType;
public PresentPropertyPolymorphicDeserializer(Class<T> vc) {
super(vc);
this.propertyNameToType = Arrays.stream(vc.getAnnotation(JsonSubTypes.class).value())
.collect(Collectors.toMap(Type::name, Type::value,
(a, b) -> a, LinkedHashMap::new)); // LinkedHashMap to support precedence case by definition order
}
@Override
public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
ObjectMapper objectMapper = (ObjectMapper) p.getCodec();
ObjectNode object = objectMapper.readTree(p);
for (String propertyName : propertyNameToType.keySet()) {
if (object.has(propertyName)) {
return deserialize(objectMapper, propertyName, object);
}
}
throw new IllegalArgumentException("could not infer to which class to deserialize " + object);
}
@SuppressWarnings("unchecked")
private T deserialize(ObjectMapper objectMapper,
String propertyName,
ObjectNode object) throws IOException {
return (T) objectMapper.treeToValue(object, propertyNameToType.get(propertyName));
}
}
Exemple d'utilisation:
@JsonSubTypes({
@JsonSubTypes.Type(value = Foo.class, name = "foo"),
@JsonSubTypes.Type(value = Bar.class, name = "bar"),
})
interface FooBar {
}
@AllArgsConstructor(onConstructor_ = @JsonCreator)
@Value
static class Foo implements FooBar {
private final String foo;
}
@AllArgsConstructor(onConstructor_ = @JsonCreator)
@Value
static class Bar implements FooBar {
private final String bar;
}
Configuration de Jackson
SimpleModule module = new SimpleModule();
module.addDeserializer(FooBar.class, new PresentPropertyPolymorphicDeserializer<>(FooBar.class));
objectMapper.registerModule(module);
ou si vous utilisez Spring Boot:
@JsonComponent
public class FooBarDeserializer extends PresentPropertyPolymorphicDeserializer<FooBar> {
public FooBarDeserializer() {
super(FooBar.class);
}
}
Tests:
@Test
void shouldDeserializeFoo() throws IOException {
// given
var json = "{\"foo\":\"foo\"}";
// when
var actual = objectMapper.readValue(json, FooBar.class);
// then
then(actual).isEqualTo(new Foo("foo"));
}
@Test
void shouldDeserializeBar() throws IOException {
// given
var json = "{\"bar\":\"bar\"}";
// when
var actual = objectMapper.readValue(json, FooBar.class);
// then
then(actual).isEqualTo(new Bar("bar"));
}
@Test
void shouldDeserializeUsingAnnotationDefinitionPrecedenceOrder() throws IOException {
// given
var json = "{\"bar\":\"\", \"foo\": \"foo\"}";
// when
var actual = objectMapper.readValue(json, FooBar.class);
// then
then(actual).isEqualTo(new Foo("foo"));
}
Mon application me demande de conserver l'ancienne structure, j'ai donc trouvé un moyen de prendre en charge le polymorphisme sans modifier les données. Voici ce que je fais:
Convertir en arbre et lire le champ, puis renvoyer l'objet de sous-classe
@Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonNode jsonNode = p.readValueAsTree();
Iterator<Map.Entry<String, JsonNode>> ite = jsonNode.fields();
boolean isSubclass = false;
while (ite.hasNext()) {
Map.Entry<String, JsonNode> entry = ite.next();
// **Check if it contains field name unique to subclass**
if (entry.getKey().equalsIgnoreCase("Field-Unique-to-Subclass")) {
isSubclass = true;
break;
}
}
if (isSubclass) {
return mapper.treeToValue(jsonNode, SubClass.class);
} else {
// process other classes
}
}