Jackson: Comment append une propriété personnalisée au JSON sans modifier le POJO

Je développe une interface REST pour mon application utilisant Jackson pour sérialiser mes objects de domaine POJO en représentation JSON. Je souhaite personnaliser la sérialisation de certains types pour append des propriétés supplémentaires à la représentation JSON qui n’existent pas dans les POJO (par exemple, append des métadonnées, des données de référence, etc.). Je sais comment écrire mon propre JsonSerializer , mais dans ce cas, je devrais appeler explicitement les JsonGenerator.writeXXX(..) pour chaque propriété de mon object alors que tout ce dont j’ai besoin est juste d’ append une propriété supplémentaire. En d’autres termes, je voudrais pouvoir écrire quelque chose comme:

 @Override public void serialize(TaxonomyNode value, JsonGenerator jgen, SerializerProvider provider) { jgen.writeStartObject(); jgen.writeAllFields(value); // <-- The method I'd like to have jgen.writeObjectField("my_extra_field", "some data"); jgen.writeEndObject(); } 

ou (encore mieux) pour intercepter en quelque sorte la sérialisation avant l’appel jgen.writeEndObject() , par exemple:

 @Override void beforeEndObject(....) { jgen.writeObjectField("my_extra_field", "some data"); } 

Je pensais pouvoir étendre BeanSerializer et remplacer sa méthode de serialize(..) mais elle a été déclarée final et je n’ai pas trouvé de moyen facile de créer une nouvelle instance de BeanSerializer sans lui fournir tous les détails de métadonnées en dupliquant une bonne partie de Jackson. Alors j’ai renoncé à le faire.

Ma question est la suivante : comment personnaliser la sérialisation de Jackson pour append des éléments supplémentaires à la sortie JSON de certains POJO particuliers sans introduire trop de code passe-partout et réutiliser autant que possible le comportement par défaut de Jackson.

Depuis (je pense) Jackson 1.7, vous pouvez le faire avec un BeanSerializerModifier et étendre BeanSerializerBase . J’ai testé l’exemple ci-dessous avec Jackson 2.0.4.

 import java.io.IOException; import org.junit.Test; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.BeanDescription; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; import com.fasterxml.jackson.databind.ser.impl.ObjectIdWriter; import com.fasterxml.jackson.databind.ser.std.BeanSerializerBase; public class JacksonSerializeWithExtraField { @Test public void testAddExtraField() throws Exception { ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new SimpleModule() { public void setupModule(SetupContext context) { super.setupModule(context); context.addBeanSerializerModifier(new BeanSerializerModifier() { public JsonSerializer modifySerializer( SerializationConfig config, BeanDescription beanDesc, JsonSerializer serializer) { if (serializer instanceof BeanSerializerBase) { return new ExtraFieldSerializer( (BeanSerializerBase) serializer); } return serializer; } }); } }); mapper.writeValue(System.out, new MyClass()); //prints {"classField":"classFieldValue","extraField":"extraFieldValue"} } class MyClass { private Ssortingng classField = "classFieldValue"; public Ssortingng getClassField() { return classField; } public void setClassField(Ssortingng classField) { this.classField = classField; } } class ExtraFieldSerializer extends BeanSerializerBase { ExtraFieldSerializer(BeanSerializerBase source) { super(source); } ExtraFieldSerializer(ExtraFieldSerializer source, ObjectIdWriter objectIdWriter) { super(source, objectIdWriter); } ExtraFieldSerializer(ExtraFieldSerializer source, Ssortingng[] toIgnore) { super(source, toIgnore); } protected BeanSerializerBase withObjectIdWriter( ObjectIdWriter objectIdWriter) { return new ExtraFieldSerializer(this, objectIdWriter); } protected BeanSerializerBase withIgnorals(Ssortingng[] toIgnore) { return new ExtraFieldSerializer(this, toIgnore); } public void serialize(Object bean, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException { jgen.writeStartObject(); serializeFields(bean, jgen, provider); jgen.writeSsortingngField("extraField", "extraFieldValue"); jgen.writeEndObject(); } } } 

Jackson 2.5 a introduit l’annotation @JsonAppend , qui peut être utilisée pour append des propriétés “virtuelles” lors de la sérialisation. Il peut être utilisé avec la fonctionnalité mixin pour éviter de modifier le POJO d’origine.

L’exemple suivant ajoute une propriété ApprovalState lors de la sérialisation:

 @JsonAppend( attrs = { @JsonAppend.Attr(value = "ApprovalState") } ) public static class ApprovalMixin {} 

Enregistrez le mixin avec ObjectMapper :

 mapper.addMixIn(POJO.class, ApprovalMixin.class); 

Utilisez un ObjectWriter pour définir l’atsortingbut lors de la sérialisation:

 ObjectWriter writer = mapper.writerFor(POJO.class) .withAtsortingbute("ApprovalState", "Pending"); 

L’utilisation du graveur pour la sérialisation appenda le champ ApprovalState à la sortie.

Vous pouvez le faire (la version précédente ne fonctionnait pas avec Jackson après 2.6, mais cela fonctionne avec Jackson 2.7.3):

 public static class CustomModule extends SimpleModule { public CustomModule() { addSerializer(CustomClass.class, new CustomClassSerializer()); } private static class CustomClassSerializer extends JsonSerializer { @Override public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException { //Validate.isInstanceOf(CustomClass.class, value); jgen.writeStartObject(); JavaType javaType = provider.constructType(CustomClass.class); BeanDescription beanDesc = provider.getConfig().introspect(javaType); JsonSerializer serializer = BeanSerializerFactory.instance.findBeanSerializer(provider, javaType, beanDesc); // this is basically your 'writeAllFields()'-method: serializer.unwrappingSerializer(null).serialize(value, jgen, provider); jgen.writeObjectField("my_extra_field", "some data"); jgen.writeEndObject(); } } } 

Bien que cette question ait déjà été résolue, j’ai trouvé un autre moyen qui ne nécessite aucun crochet spécial Jackson.

 static class JsonWrapper { @JsonUnwrapped private T inner; private Ssortingng extraField; public JsonWrapper(T inner, Ssortingng field) { this.inner = inner; this.extraField = field; } public T getInner() { return inner; } public Ssortingng getExtraField() { return extraField; } } static class BaseClass { private Ssortingng baseField; public BaseClass(Ssortingng baseField) { this.baseField = baseField; } public Ssortingng getBaseField() { return baseField; } } public static void main(Ssortingng[] args) throws JsonProcessingException { Object input = new JsonWrapper<>(new BaseClass("inner"), "outer"); System.out.println(new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(input)); } 

Les sorties:

 { "baseField" : "inner", "extraField" : "outer" } 

Pour écrire des collections, vous pouvez simplement utiliser une vue:

 public static void main(Ssortingng[] args) throws JsonProcessingException { List inputs = Arrays.asList(new BaseClass("1"), new BaseClass("2")); //Google Guava Library <3 List> modInputs = Lists.transform(inputs, base -> new JsonWrapper<>(base, "hello")); System.out.println(new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(modInputs)); } 

Sortie:

 [ { "baseField" : "1", "extraField" : "hello" }, { "baseField" : "2", "extraField" : "hello" } ] 

Nous pouvons utiliser la reflection pour obtenir tous les champs de l’object que vous souhaitez parsingr.

 @JsonSerialize(using=CustomSerializer.class) class Test{ int id; Ssortingng name; Ssortingng hash; } 

Dans le sérialiseur personnalisé, nous avons notre méthode de sérialisation comme ceci:

  @Override public void serialize(Test value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { jgen.writeStartObject(); Field[] fields = value.getClass().getDeclaredFields(); for (Field field : fields) { try { jgen.writeObjectField(field.getName(), field.get(value)); } catch (IllegalArgumentException | IllegalAccessException e) { e.printStackTrace(); } } jgen.writeObjectField("extra_field", "whatever_value"); jgen.writeEndObject(); } 

Inspiré de ce que wajda a dit et écrit dans cet aperçu :

Voici comment append un écouteur pour la sérialisation du bean dans jackson 1.9.12. Dans cet exemple, le listerner est considéré comme une chaîne de commande dont l’interface est:

 public interface BeanSerializerListener { void postSerialization(Object value, JsonGenerator jgen) throws IOException; } 

MyBeanSerializer.java:

 public class MyBeanSerializer extends BeanSerializerBase { private final BeanSerializerListener serializerListener; protected MyBeanSerializer(final BeanSerializerBase src, final BeanSerializerListener serializerListener) { super(src); this.serializerListener = serializerListener; } @Override public void serialize(final Object bean, final JsonGenerator jgen, final SerializerProvider provider) throws IOException, JsonGenerationException { jgen.writeStartObject(); if (_propertyFilterId != null) { serializeFieldsFiltered(bean, jgen, provider); } else { serializeFields(bean, jgen, provider); } serializerListener.postSerialization(bean, jgen); jgen.writeEndObject(); } } 

MyBeanSerializerBuilder.java:

 public class MyBeanSerializerBuilder extends BeanSerializerBuilder { private final BeanSerializerListener serializerListener; public MyBeanSerializerBuilder(final BasicBeanDescription beanDesc, final BeanSerializerListener serializerListener) { super(beanDesc); this.serializerListener = serializerListener; } @Override public JsonSerializer build() { BeanSerializerBase src = (BeanSerializerBase) super.build(); return new MyBeanSerializer(src, serializerListener); } } 

MyBeanSerializerFactory.java:

 public class MyBeanSerializerFactory extends BeanSerializerFactory { private final BeanSerializerListener serializerListener; public MyBeanSerializerFactory(final BeanSerializerListener serializerListener) { super(null); this.serializerListener = serializerListener; } @Override protected BeanSerializerBuilder constructBeanSerializerBuilder(final BasicBeanDescription beanDesc) { return new MyBeanSerializerBuilder(beanDesc, serializerListener); } } 

La dernière classe ci-dessous montre comment le fournir avec Resteasy 3.0.7:

 @Provider public class ObjectMapperProvider implements ContextResolver { private final MapperConfigurator mapperCfg; public ObjectMapperProvider() { mapperCfg = new MapperConfigurator(null, null); mapperCfg.setAnnotationsToUse(new Annotations[]{Annotations.JACKSON, Annotations.JAXB}); mapperCfg.getConfiguredMapper().setSerializerFactory(serializerFactory); } @Override public ObjectMapper getContext(final Class type) { return mapperCfg.getConfiguredMapper(); } } 

Nous pouvons étendre BeanSerializer , mais avec peu de ruse.

Tout d’abord, définissez une classe Java pour encapsuler votre POJO.

 @JsonSerialize(using = MixinResultSerializer.class) public class MixinResult { private final Object origin; private final Map mixed = Maps.newHashMap(); @JsonCreator public MixinResult(@JsonProperty("origin") Object origin) { this.origin = origin; } public void add(Ssortingng key, Ssortingng value) { this.mixed.put(key, value); } public Map getMixed() { return mixed; } public Object getOrigin() { return origin; } } 

Ensuite, implémentez votre serializer personnalisé.

 public final class MixinResultSerializer extends BeanSerializer { public MixinResultSerializer() { super(SimpleType.construct(MixinResult.class), null, new BeanPropertyWriter[0], new BeanPropertyWriter[0]); } public MixinResultSerializer(BeanSerializerBase base) { super(base); } @Override protected void serializeFields(Object bean, JsonGenerator gen, SerializerProvider provider) throws IOException { if (bean instanceof MixinResult) { MixinResult mixin = (MixinResult) bean; Object origin = mixin.getOrigin(); BeanSerializer serializer = (BeanSerializer) provider.findValueSerializer(SimpleType.construct(origin.getClass())); new MixinResultSerializer(serializer).serializeFields(origin, gen, provider); mixin.getMixed().entrySet() .stream() .filter(entry -> entry.getValue() != null) .forEach((entry -> { try { gen.writeFieldName(entry.getKey()); gen.writeRawValue(entry.getValue()); } catch (IOException e) { throw new RuntimeException(e); } })); } else { super.serializeFields(bean, gen, provider); } } } 

De cette façon, nous pouvons gérer l’object qui origine l’object en utilisant les annotations jackson pour un comportement de sérialisation personnalisé.

Une autre solution, peut-être la plus simple:

Faites de la sérialisation un processus en 2 étapes. Commencez par créer une Map comme:

 Map map = req.mapper().convertValue( result, new TypeReference>() {} ); 

puis ajoutez les propriétés que vous voulez comme:

 map.put( "custom", "value" ); 

puis sérialisez ceci à json:

 Ssortingng json = req.mapper().writeValueAsSsortingng( map ); 

Pour mon cas d’utilisation, je pourrais utiliser un moyen beaucoup plus simple. Dans une classe de base que j’ai pour tous mes “Jackson Pojos”, j’ajoute:

 protected Map dynamicProperties = new HashMap(); ... public Object get(Ssortingng name) { return dynamicProperties.get(name); } // "any getter" needed for serialization @JsonAnyGetter public Map any() { return dynamicProperties; } @JsonAnySetter public void set(Ssortingng name, Object value) { dynamicProperties.put(name, value); } 

Je peux maintenant désérialiser à Pojo, travailler avec des champs et re-sérialiser sans perdre de propriétés. Je peux aussi append / modifier les propriétés non pojo:

 // Pojo fields person.setFirstName("Annna"); // Dynamic field person.set("ex", "test"); 

(Obtenu de Cowtowncoder )

J’avais besoin de cette capacité aussi; dans mon cas, pour soutenir l’expansion sur le terrain sur les services REST. J’ai fini par développer un petit framework pour résoudre ce problème, et c’est open source sur github . Il est également disponible dans le référentiel central de maven .

Il s’occupe de tout le travail. Enveloppez simplement le POJO dans un MorphedResult, puis ajoutez ou supprimez des propriétés à volonté. Lorsque sérialisé, le wrapper MorphedResult disparaît et tous les “changements” apparaissent dans l’object JSON sérialisé.

 MorphedResult result = new MorphedResult<>(pojo); result.addExpansionData("my_extra_field", "some data"); 

Voir la page github pour plus de détails et d’exemples. Veillez à enregistrer le filtre ‘library’ avec le mappeur d’object de Jackson comme suit:

 ObjectMapper mapper = new ObjectMapper(); mapper.setFilters(new FilteredResultProvider()); 

Après avoir cherché plus sur le code source de Jackson, j’ai conclu qu’il était tout simplement impossible de réaliser sans écrire mes propres BeanSerializer , BeanSerializerBuilder et BeanSerializerFactory et fournir des points d’extension tels que:

 /* /********************************************************** /* Extension points /********************************************************** */ protected void beforeEndObject(T bean, JsonGenerator jgen, SerializerProvider provider) throws IOException, JSONException { // May be overridden } protected void afterStartObject(T bean, JsonGenerator jgen, SerializerProvider provider) throws IOException, JSONException { // May be overridden } 

Malheureusement, j’ai dû copier et coller tout le code source de MyCustomBeanSerializer de MyCustomBeanSerializer dans MyCustomBeanSerializer car le premier n’est pas développé pour les extensions déclarant tous les champs et certaines méthodes importantes (comme serialize(...) ) comme final