web-dev-qa-db-fra.com

Jackson: Comment ajouter 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 objets de domaine POJO en représentation JSON. Je souhaite personnaliser la sérialisation de certains types pour ajouter des propriétés supplémentaires à la représentation JSON qui n'existent pas dans les POJO (par exemple, ajouter des métadonnées, des données de référence, etc.). Je sais comment écrire ma propre JsonSerializer, mais dans ce cas, je devrais appeler explicitement les méthodes JsonGenerator.writeXXX(..) pour la propriété each de mon objet, alors que tout ce dont j'ai besoin est simplement de ajouter une propriété supplémentaire. En d'autres termes, j'aimerais 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) d'intercepter 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 serialize(..), mais elle est déclarée final et je ne pouvais pas non plus trouver un moyen facile de créer une nouvelle instance de BeanSerializer sans lui fournir tous les détails de métadonnées de type dupliquant pratiquement une bonne partie de Jackson. Donc j'ai abandonné ça.

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

47
Alex Vayda

Depuis (je pense) Jackson 1.7, vous pouvez le faire avec une variable BeanSerializerModifier et une extension 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 String classField = "classFieldValue";

        public String getClassField() { 
            return classField; 
        }
        public void setClassField(String classField) { 
            this.classField = classField; 
        }
    }


    class ExtraFieldSerializer extends BeanSerializerBase {

        ExtraFieldSerializer(BeanSerializerBase source) {
            super(source);
        }

        ExtraFieldSerializer(ExtraFieldSerializer source, 
                ObjectIdWriter objectIdWriter) {
            super(source, objectIdWriter);
        }

        ExtraFieldSerializer(ExtraFieldSerializer source, 
                String[] toIgnore) {
            super(source, toIgnore);
        }

        protected BeanSerializerBase withObjectIdWriter(
                ObjectIdWriter objectIdWriter) {
            return new ExtraFieldSerializer(this, objectIdWriter);
        }

        protected BeanSerializerBase withIgnorals(String[] 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.writeStringField("extraField", "extraFieldValue"); 
            jgen.writeEndObject();
        }
    }
}
35
ryanp

Jackson 2.5 a introduit l'annotation @JsonAppend , qui peut être utilisé pour ajouter 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 le ObjectMapper:

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

Utilisez une ObjectWriter pour définir l'attribut lors de la sérialisation:

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

L'utilisation de l'enregistreur pour la sérialisation ajoute le champ ApprovalState à la sortie.

20

Vous pouvez faire cela (la version précédente ne fonctionnait pas avec Jackson après la version 2.6, mais cela fonctionnait 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<Object> 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();
        }
    }
}
12
Rasmus Faber

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

static class JsonWrapper<T> {
    @JsonUnwrapped
    private T inner;
    private String extraField;

    public JsonWrapper(T inner, String field) {
        this.inner = inner;
        this.extraField = field;
    }

    public T getInner() {
        return inner;
    }

    public String getExtraField() {
        return extraField;
    }
}

static class BaseClass {
    private String baseField;

    public BaseClass(String baseField) {
        this.baseField = baseField;
    }

    public String getBaseField() {
        return baseField;
    }
}

public static void main(String[] 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(String[] args) throws JsonProcessingException {
    List<BaseClass> inputs = Arrays.asList(new BaseClass("1"), new BaseClass("2"));
    //Google Guava Library <3
    List<JsonWrapper<BaseClass>> 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"
} ]
9
DieterDP

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

protected Map<String,Object> dynamicProperties = new HashMap<String,Object>();

...


public Object get(String name) {
    return dynamicProperties.get(name);
}

// "any getter" needed for serialization    
@JsonAnyGetter
public Map<String,Object> any() {
    return dynamicProperties;
}

@JsonAnySetter
public void set(String name, Object value) {
    dynamicProperties.put(name, value);
}

Je peux maintenant désérialiser sur Pojo, travailler avec des champs et resérialiser avec perdre toutes les propriétés. Je peux aussi ajouter/changer les propriétés non-pojo:

// Pojo fields
person.setFirstName("Annna");

// Dynamic field
person.set("ex", "test");

(Obtenu de Cowtowncoder )

2
Brimstedt

Inspiré de ce que wajda a dit et écrit dans ce Gist :

Voici comment ajouter un écouteur pour la sérialisation de 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 à l'aide de Resteasy 3.0.7:

@Provider
public class ObjectMapperProvider implements ContextResolver<ObjectMapper> {
    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();
    }
}
1
Charlouze

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

Faites de la sérialisation un processus en deux étapes. Commencez par créer un Map<String,Object> comme:

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

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

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

puis sérialiser ceci à json:

String json = req.mapper().writeValueAsString( map );
1
Scheintod

Nous pouvons utiliser la réflexion pour obtenir tous les champs de l'objet que vous souhaitez analyser.

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

Dans le sérialiseur personnalisé, notre méthode de sérialisation est la suivante: 

        @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();

        }
1
Sourabh

Nous pouvons prolonger BeanSerializer, mais avec peu d’astuce.

Commencez par définir une classe Java pour encapsuler votre POJO.

@JsonSerialize(using = MixinResultSerializer.class)
public class MixinResult {

    private final Object Origin;
    private final Map<String, String> mixed = Maps.newHashMap();

    @JsonCreator
    public MixinResult(@JsonProperty("Origin") Object Origin) {
        this.Origin = Origin;
    }

    public void add(String key, String value) {
        this.mixed.put(key, value);
    }

    public Map<String, String> getMixed() {
        return mixed;
    }

    public Object getOrigin() {
        return Origin;
    }

}

Ensuite, implémentez votre serializer personnalisée.

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 le cas où l'objet Origin utilise des annotations de Jackson pour personnaliser le comportement de sérialisation.

1
smartwjw

J'avais aussi besoin de cette capacité. dans mon cas, prendre en charge le développement de champs sur les services REST. J'ai fini par développer un cadre minuscule pour résoudre ce problème, qui est une source ouverte sur github . Il est également disponible dans le répertoire maven .

Il prend en charge tout le travail. Enroulez simplement le POJO dans un MorphedResult, puis ajoutez ou supprimez des propriétés à volonté. Lors de la sérialisation, l'encapsuleur MorphedResult disparaît et les modifications apportées apparaissent dans l'objet 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. Assurez-vous d'enregistrer le "filtre" des bibliothèques avec le mappeur d'objets de Jackson comme ceci:

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