Jackson: Cómo agregar propiedades personalizadas al JSON sin modificar el POJO

Estoy desarrollando una interfaz REST para mi aplicación usando Jackson para serializar mis objetos de dominio POJO a la representación JSON. Deseo personalizar la serialización para algunos tipos para agregar propiedades adicionales a la representación JSON que no existen en POJO (por ejemplo, agregar algunos metadatos, datos de referencia, etc.). Sé cómo escribir mi propio JsonSerializer , pero en ese caso necesitaría llamar explícitamente a JsonGenerator.writeXXX(..) métodos para cada propiedad de mi objeto mientras que todo lo que necesito es solo agregar una propiedad adicional. En otras palabras, me gustaría poder escribir algo como:

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

o (aún mejor) para interceptar de algún modo la serialización antes de la llamada a jgen.writeEndObject() , por ejemplo:

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

Pensé que podría extender BeanSerializer y anular su serialize(..) método, pero se declaró final y tampoco pude encontrar una manera fácil de crear una nueva instancia de BeanSerializer sin proporcionarle todos los datos de metadatos de tipo que prácticamente duplican una buena parte de Jackson. Así que he renunciado a hacer eso.

Mi pregunta es : cómo personalizar la serialización de Jackson para agregar cosas adicionales a la salida JSON para POJO particulares sin introducir demasiado del código repetitivo y reutilizar tanto como sea posible el comportamiento predeterminado de Jackson.

Desde (creo) Jackson 1.7 puedes hacer esto con un BeanSerializerModifier y extender BeanSerializerBase . He probado el ejemplo a continuación con 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 introdujo la anotación @JsonAppend , que se puede usar para agregar propiedades “virtuales” durante la serialización. Se puede usar con la funcionalidad mixin para evitar modificar el POJO original.

El siguiente ejemplo agrega una propiedad ApprovalState durante la serialización:

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

Registra el mixin con ObjectMapper :

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

Use un ObjectWriter para establecer el atributo durante la serialización:

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

El uso del escritor para la serialización agregará el campo ApprovalState a la salida.

Puedes hacer esto (la versión anterior no funcionó con Jackson después de la versión 2.6, pero esto funciona con 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(); } } } 

Aunque esta pregunta ya fue respondida, encontré otra manera que no requiere ganchos Jackson especiales.

 static class JsonWrapper { @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)); } 

Productos:

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

Para escribir colecciones, simplemente puede usar una vista:

 public static void main(String[] 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)); } 

Salida:

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

Podemos usar la reflexión para obtener todos los campos del objeto que queremos analizar.

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

En el serializador personalizado, tenemos nuestro método de serialización como este:

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

Inspirado por lo que Wajda dijo y escribió en esta esencia :

Aquí es cómo agregar un oyente para la serialización de frijoles en jackson 1.9.12. En este ejemplo, el listerner se considera como una cadena de comando cuya interfaz es:

 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 última clase a continuación muestra cómo proporcionarla usando 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(); } } 

Podemos extender BeanSerializer , pero con un pequeño truco.

Primero, defina una clase java para envolver su 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(String key, String value) { this.mixed.put(key, value); } public Map getMixed() { return mixed; } public Object getOrigin() { return origin; } } 

Luego, implemente su serializer personalizado.

 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 esta forma, podemos manejar el caso de que el objeto de origen utilice las anotaciones de jackson para personalizar el comportamiento de serialización.

Otra y quizás la solución más simple:

Haga que la serialización sea un proceso de 2 pasos. Primero crea un Map como:

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

luego agrega las propiedades que quieras como:

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

luego serializar esto a json:

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

Para mi caso de uso, podría usar una forma mucho más simple. En una de las clases base que tengo para todos mis “Jackson Pojos”, agrego:

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

Ahora puedo deserializar a Pojo, trabajar con campos y reserializar sin perder ninguna propiedad. También puedo agregar / cambiar propiedades no pojo:

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

(Lo tengo de Cowtowncoder )

Necesitaba esta habilidad también; en mi caso, para apoyar la expansión de campo en los servicios REST. Terminé desarrollando un pequeño marco para resolver este problema, y ​​está abierto en github . También está disponible en el repository central de maven .

Se ocupa de todo el trabajo. Simplemente ajuste el POJO en MorphedResult, y luego agregue o elimine las propiedades a voluntad. Cuando se serializa, el contenedor MorphedResult desaparece y aparecen ‘cambios’ en el objeto JSON serializado.

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

Vea la página de github para más detalles y ejemplos. Asegúrese de registrar el filtro de las bibliotecas con el mapeador de objetos de Jackson así:

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

Después de mirar más sobre el código fuente de Jackson , concluí que es simplemente imposible de lograr sin escribir mi propio BeanSerializer , BeanSerializerBuilder y BeanSerializerFactory y proporcionar algunos puntos de extensión como:

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

Desafortunadamente tuve que copiar y pegar todo el código fuente de BeanSerializer Jackson en MyCustomBeanSerializer porque el primero no está desarrollado para extensiones que declaran todos los campos y algunos métodos importantes (como serialize(...) ) como final