Jackson: comment ajouter une propriété personnalisée au JSON sans modifier le POJO

je développe une interface de repos pour mon application en utilisant Jackson pour sérialiser mes objets de domaine POJO à la représentation JSON. Je veux personnaliser la sérialisation pour certains types pour ajouter des propriétés supplémentaires à la représentation JSON qui n'existent pas dans POJOs (par exemple ajouter 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 explicitement appeler JsonGenerator.writeXXX(..) méthodes pour propriété de mon objet alors que tout ce dont j'ai besoin est juste pour ajouter 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) d'intercepter la sérialisation avant le jgen.writeEndObject() appel, par exemple:

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

je pensais que je pouvais étendre BeanSerializer et de remplacer son serialize(..) méthode mais elle est déclarée final et aussi je ne pouvais pas trouver un moyen facile de créer une nouvelle instance de BeanSerializer sans lui fournir tous les détails des métadonnées de type pratiquement dupliquer une bonne partie de Jackson. Donc, j'ai renoncé à le faire.

ma question Est - comment personnaliser la sérialisation de Jackson pour ajouter des éléments supplémentaires à la sortie JSON pour des POJOs particuliers sans introduire trop de code boilerplate et en réutilisant autant que possible le comportement par défaut de Jackson.

42
demandé sur JJD 2013-02-05 22:28:10

11 réponses

puisque (je pense) Jackson 1.7 vous pouvez faire cela avec un BeanSerializerModifier et l'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
répondu ryanp 2014-10-10 17:18:13

Jackson 2.5 introduit le @JsonAppend annotation, qui peut être utilisée 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 un ApprovalState propriété pendant 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 un ObjectWriter pour définir l'attribut lors de la sérialisation:

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

à l'Aide de l'écrivain pour la sérialisation ajouter le ApprovalState champ à la sortie.

16
répondu Henrik Aasted 2017-02-25 11:27:20

Vous pouvez faire ceci (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<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();
        }
    }
}
11
répondu Rasmus Faber 2016-04-26 09:16:10

bien que cette question soit déjà répondue, j'ai trouvé une autre façon qui ne nécessite pas de crochets Jackson Spéciaux.

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));
}

extrants:

{
  "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"
} ]
7
répondu DieterDP 2016-03-31 12:40:45

On peut 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 la coutume sérialiseur, nous avons notre méthode serialize 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();

        }
1
répondu Sourabh 2014-10-16 11:38:10

inspiré de ce que wajda a dit et écrit dans ce gist:

Voici comment ajouter un écouteur pour la sérialisation des haricots dans jackson 1.9.12. Dans cet exemple, le listner est considéré comme une chaîne de commandement 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 catégorie ci-dessous montre comment fournir utilisation 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
répondu Charlouze 2015-10-21 12:22:10

Nous pouvons étendre BeanSerializer, mais avec un peu d'astuce.

tout d'abord, définissez une classe java pour envelopper 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;
    }

}

alors, mettez en œuvre votre coutume serializer.

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 que l'objet origin en utilisant des annotations jackson pour personnaliser le comportement de serialize.

1
répondu smartwjw 2016-05-06 02:18:31

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

faire de la sérialisation un processus en 2 étapes. Tout d'abord 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érialisez ceci à json:

String json = req.mapper().writeValueAsString( map );
1
répondu Scheintod 2018-02-20 12:44:18

pour mon cas d'utilisation, je pourrais utiliser une façon beaucoup plus simple. Dans la classe de base j'ai pour tous mes "Pojos Jackson" 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 Pojo, travailler avec fields et resérialiser witjout sans perdre de propriétés. Je peux aussi ajouter/modifier non pojo propriétés:

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

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

(Obtenu à partir de Cowtowncoder)

1
répondu Brimstedt 2018-05-16 05:30:30

j'avais aussi besoin de cette capacité; dans mon cas, pour soutenir l'expansion sur le terrain sur les services de repos. J'ai fini par développer un petit cadre pour résoudre ce problème, et il est open source sur github. Il est également disponible dans les Maven central repository.

il s'occupe de tout le travail. Simplement envelopper le POJO dans un MorphedResult, puis Ajouter ou supprimer des propriétés à volonté. Une fois sérialisé, le papier D'emballage MorphedResult disparaît et tout "changement" apparaît dans le 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 des exemples. Assurez-vous d'enregistrer le 'filtre' des bibliothèques avec le mapper D'objets de Jackson comme ceci:

ObjectMapper mapper = new ObjectMapper();
mapper.setFilters(new FilteredResultProvider());
0
répondu allenru 2015-03-16 21:28:36

Après avoir regardé de plus sur le Jackson code source j'ai conclu qu'il est tout simplement impossible de réaliser sans écrire mon propre BeanSerializer,BeanSerializerBuilder et BeanSerializerFactory et de fournir des points d'extension de la forme:

/*
/**********************************************************
/* 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 ensemble JacksonBeanSerializer code source à MyCustomBeanSerializer parce que le premier n'est pas développé pour les extensions déclarant tous les champs et certaines méthodes importantes (comme serialize(...)final

-1
répondu Alex Vayda 2013-02-07 13:28:50