it-swarm.com.de

Jackson: Hinzufügen einer benutzerdefinierten Eigenschaft zum JSON, ohne das POJO zu ändern

Ich entwickle eine REST - Schnittstelle für meine App, die Jackson verwendet, um meine POJO-Domänenobjekte in eine JSON-Darstellung zu serialisieren. Ich möchte die Serialisierung für einige Typen anpassen, um der JSON-Darstellung zusätzliche Eigenschaften hinzuzufügen, die nicht in POJOs vorhanden sind (z. B. Hinzufügen einiger Metadaten, Referenzdaten usw.). Ich weiß, wie ich meine eigene Variable JsonSerializer schreibe, aber in diesem Fall müsste ich JsonGenerator.writeXXX(..) Methoden explizit für jede - Eigenschaft meines Objekts aufrufen, während ich lediglich eine zusätzliche Eigenschaft add benötige. Mit anderen Worten würde ich gerne etwas schreiben können wie:

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

oder (noch besser), um die Serialisierung vor dem jgen.writeEndObject()-Aufruf irgendwie abzufangen, z.

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

Ich dachte, ich könnte BeanSerializer erweitern und die serialize(..)-Methode außer Kraft setzen, aber es wurde als final deklariert. Außerdem konnte ich keine einfache Möglichkeit zum Erstellen einer neuen Instanz von BeanSerializer finden, ohne sie mit allen Typ-Metadaten-Details zu versehen, die praktisch einen guten Teil von Jackson duplizieren. Also habe ich das aufgegeben.

Meine Frage ist - wie man Jacksons Serialisierung anpassen kann, um der JSON-Ausgabe für bestimmte POJOs zusätzliches Material hinzuzufügen, ohne zu viel Boilerplate-Code einzuführen und das standardmäßige Jackson-Verhalten so weit wie möglich wiederzuverwenden.

47
Alex Vayda

Seit (ich denke) Jackson 1.7 kann man dies mit einer BeanSerializerModifier und einer Erweiterung von BeanSerializerBase machen. Ich habe das Beispiel unten mit Jackson 2.0.4 getestet.

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 führte die Annotation @JsonAppend ein, mit der "virtuelle" Eigenschaften während der Serialisierung hinzugefügt werden können. Sie kann mit der Mixin-Funktion verwendet werden, um zu verhindern, dass der ursprüngliche POJO geändert wird.

Im folgenden Beispiel wird während der Serialisierung eine ApprovalState-Eigenschaft hinzugefügt:

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

Registrieren Sie das Mixin mit der ObjectMapper:

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

Verwenden Sie eine ObjectWriter, um das Attribut während der Serialisierung festzulegen:

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

Wenn Sie den Writer für die Serialisierung verwenden, wird das ApprovalState-Feld zum Ausgang hinzugefügt.

Sie können dies tun (vorherige Version funktionierte nicht mit Jackson nach 2.6, aber dies funktioniert mit 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

Obwohl diese Frage bereits beantwortet ist, habe ich einen anderen Weg gefunden, der keine speziellen Jackson-Haken erfordert.

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

Ausgänge:

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

Zum Schreiben von Sammlungen können Sie einfach eine Ansicht verwenden:

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

Ausgabe:

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

Für meinen Anwendungsfall könnte ich einen viel einfacheren Weg verwenden. In der Basisklasse habe ich für alle meine "Jackson Pojos" hinzugefügt:

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

Ich kann jetzt zu Pojo deserialisieren, mit Feldern arbeiten und ohne Verlust von Eigenschaften reserialisieren. Ich kann auch Nicht-Pojo-Eigenschaften hinzufügen/ändern:

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

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

(Bekam es von Cowtowncoder )

2
Brimstedt

Inspiriert von dem, was wajda hier geschrieben und geschrieben hat Gist :

So fügen Sie in jackson 1.9.12 einen Listener für die Bean-Serialisierung hinzu. In diesem Beispiel wird der Listner als Befehlskette mit folgender Schnittstelle betrachtet:

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

Die letzte Klasse zeigt, wie Sie sie mit Resteasy 3.0.7 bereitstellen können:

@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

Eine andere und vielleicht einfachste Lösung:

Machen Sie die Serialisierung in zwei Schritten. Erstellen Sie zuerst einen Map<String,Object> wie:

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

fügen Sie dann die gewünschten Eigenschaften hinzu:

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

dann serialisieren Sie dies zu json:

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

Reflection kann verwendet werden, um alle Felder des Objekts zu erhalten, die Sie analysieren möchten.

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

Im benutzerdefinierten Serialisierer haben wir unsere Serialisierungsmethode wie folgt: 

        @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

Wir können BeanSerializer erweitern, aber mit wenig Trick.

Definieren Sie zunächst eine Java-Klasse, die Ihren POJO umschließen soll.

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

}

Dann , implementieren Sie Ihre benutzerdefinierte 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);
        }

    }

}

Auf diese Weise können wir den Fall behandeln, in dem das Origin-Objekt mit Jackson-Anmerkungen zum benutzerdefinierten Serialisierungsverhalten verwendet wird.

1
smartwjw

Ich brauchte auch diese Fähigkeit. in meinem Fall zur Unterstützung der Felderweiterung bei REST - Diensten. Am Ende habe ich ein kleines Framework entwickelt, um dieses Problem zu lösen, und es ist auf github auf Open Source verfügbar. Es ist auch im zentralen Repository maven verfügbar.

Es kümmert sich um die ganze Arbeit. Wickeln Sie den POJO einfach in ein MorphedResult ein und fügen Sie nach Belieben Eigenschaften hinzu oder entfernen Sie diese. Bei der Serialisierung verschwindet der MorphedResult-Wrapper und alle 'Änderungen' erscheinen im serialisierten JSON-Objekt.

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

Siehe die github-Seite für weitere Details und Beispiele. Vergewissern Sie sich, dass Sie den Filter der Bibliotheken mit Jackson's Object Mapper wie folgt registrieren:

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