IT이야기

Jackson : POJO를 수정하지 않고 JSON에 사용자 지정 속성을 추가하는 방법

cyworld 2021. 3. 27. 22:53
반응형

Jackson : POJO를 수정하지 않고 JSON에 사용자 지정 속성을 추가하는 방법


Jackson을 사용하여 POJO 도메인 개체를 JSON 표현으로 직렬화하는 내 앱용 REST 인터페이스를 개발 중입니다. POJO에 존재하지 않는 JSON 표현에 추가 속성을 추가하기 위해 일부 유형에 대한 직렬화를 사용자 정의하고 싶습니다 (예 : 일부 메타 데이터, 참조 데이터 등 추가). 내 자신의을 작성하는 방법을 알고 JsonSerializer있지만이 경우에는 객체의 속성에 대해 명시 적으로 JsonGenerator.writeXXX(..)메서드를 호출 해야하지만 필요한 것은 속성 추가 하는 것 뿐입니다 . 즉, 다음과 같이 쓸 수 있기를 바랍니다.

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

또는 (더 나은) jgen.writeEndObject()호출 전에 어떻게 든 직렬화를 가로 챌 수 있습니다. 예 :

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

메서드를 확장 BeanSerializer하고 재정의 할 수 있다고 생각 serialize(..)했지만 선언 final되었으며 BeanSerializerJackson의 좋은 부분을 실질적으로 복제하는 모든 형식 메타 데이터 세부 정보를 제공하지 않고 는 새 인스턴스를 만드는 쉬운 방법을 찾을 수 없었 습니다. 그래서 나는 그것을 포기했습니다.

내 질문은 -너무 많은 상용구 코드를 도입하고 기본 Jackson 동작을 최대한 재사용하지 않고 특정 POJO에 대한 JSON 출력에 추가 항목을 추가하기 위해 Jackson의 직렬화를 사용자 정의하는 방법입니다.


(내 생각에) Jackson 1.7 이후로 a BeanSerializerModifier및 확장으로 이것을 할 수 있습니다 BeanSerializerBase. 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();
        }
    }
}

Jackson 2.5는 @JsonAppend직렬화 중에 "가상"속성을 추가하는 데 사용할 수있는 주석을 도입했습니다 . 원래 POJO를 수정하지 않도록 mixin 기능과 함께 사용할 수 있습니다.

다음 예제는 ApprovalState직렬화 중에 속성을 추가합니다 .

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

mixin을 다음과 같이 등록하십시오 ObjectMapper.

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

ObjectWriter직렬화 중에 속성을 설정 하려면를 사용하십시오 .

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

직렬화를 위해 작성기를 사용하면 ApprovalState출력에 필드 가 추가됩니다 .


이 질문은 이미 답변되었지만 특별한 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));
}

출력 :

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

컬렉션을 작성하려면 간단히보기를 사용할 수 있습니다.

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

산출:

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

이렇게 할 수 있습니다 (이전 버전은 2.6 이후 Jackson에서는 작동하지 않았지만 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();
        }
    }
}

최신 정보:

나는 Jackson 2.9.0과 2.9.6에서 시험해 보았고 둘 다 예상대로 작동했습니다. 아마도 이것을 시도해보십시오 : http://jdoodle.com/a/z99 (로컬에서 실행하십시오-jdoodle은 분명히 Jackson을 처리 할 수 ​​없습니다).


제 사용 사례에서는 훨씬 더 간단한 방법을 사용할 수 있습니다. 기본 클래스에서 모든 "Jackson Pojos"에 대해 다음을 추가합니다.

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

이제 Pojo로 deserialize하고, 필드로 작업하고, 속성을 잃는 witjout을 다시 직렬화 할 수 있습니다. pojo가 아닌 속성을 추가 / 변경할 수도 있습니다.

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

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

( Cowtowncoder 에서 얻었습니다 )


리플렉션을 사용하여 구문 분석하려는 개체의 모든 필드를 가져올 수 있습니다.

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

커스텀 serializer에는 다음과 같은 serialize 메서드가 있습니다.

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

        }

아마도 가장 간단한 또 ​​다른 해결책 :

직렬화를 2 단계 프로세스로 만듭니다. 먼저 다음 Map<String,Object>과 같이 만드십시오 .

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

그런 다음 원하는 속성을 추가하십시오.

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

그런 다음 이것을 json에 직렬화하십시오.

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

wajda가이 요점 에서 말하고 쓴 내용에서 영감을 얻었습니다 .

다음은 jackson 1.9.12에서 빈 직렬화를위한 리스너를 추가하는 방법입니다. 이 예에서 listerner는 인터페이스가 다음과 같은 명령 체인으로 간주됩니다.

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

아래 마지막 클래스는 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();
    }
}

확장 할 수 BeanSerializer있지만 약간의 트릭이 있습니다.

먼저 POJO를 래퍼 할 Java 클래스를 정의합니다.

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

}

그런 다음 사용자 정의를 구현하십시오 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);
        }

    }

}

이런 식으로 jackson 어노테이션을 사용하여 동작을 직렬화하는 원본 객체를 처리 할 수 ​​있습니다.


저도이 능력이 필요했습니다. 제 경우에는 REST 서비스에서 필드 확장을 지원합니다. 이 문제를 해결하기 위해 작은 프레임 워크를 개발하게되었고 github 에서 오픈 소스되었습니다 . Maven 중앙 저장소 에서도 사용할 수 있습니다 .

모든 작업을 처리합니다. MorphedResult에서 POJO를 래핑 한 다음 원하는대로 속성을 추가하거나 제거하면됩니다. 직렬화되면 MorphedResult 래퍼가 사라지고 직렬화 된 JSON 객체에 '변경 사항'이 나타납니다.

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

자세한 내용과 예제는 github 페이지를 참조하십시오. 다음과 같이 Jackson의 객체 매퍼에 라이브러리 '필터'를 등록해야합니다.

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

온 더 찾고 후 잭슨의 소스 코드를 나는 내 자신을 작성하지 않고 달성하기 위해 단순히 불가능하다는 결론을 내렸다 BeanSerializer, BeanSerializerBuilder그리고 BeanSerializerFactory와 같은 일부 확장 점을 제공합니다 :

/*
/**********************************************************
/* 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
}

불행하게도 나는 전체 복사 및 붙여 넣기했다 잭슨BeanSerializer에 소스 코드를 MyCustomBeanSerializer전자가 모든 필드를 선언 확장과 몇 가지 중요한 방법 (같은 개발되지 않았기 때문에 serialize(...)) 등final

참조 URL : https://stackoverflow.com/questions/14714328/jackson-how-to-add-custom-property-to-the-json-without-modifying-the-pojo

반응형