¿Cómo uso un serializador personalizado con Jackson?

111

Tengo dos clases de Java que quiero serializar a JSON usando Jackson:

public class User {
    public final int id;
    public final String name;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

public class Item {
    public final int id;
    public final String itemNr;
    public final User createdBy;

    public Item(int id, String itemNr, User createdBy) {
        this.id = id;
        this.itemNr = itemNr;
        this.createdBy = createdBy;
    }
}

Quiero serializar un artículo en este JSON:

{"id":7, "itemNr":"TEST", "createdBy":3}

con User serializado para incluir solo el id. También podré serilizar todos los objetos de usuario a JSON como:

{"id":3, "name": "Jonas", "email": "[email protected]"}

Así que supongo que necesito escribir un serializador personalizado Itemy probé con esto:

public class ItemSerializer extends JsonSerializer<Item> {

@Override
public void serialize(Item value, JsonGenerator jgen,
        SerializerProvider provider) throws IOException,
        JsonProcessingException {
    jgen.writeStartObject();
    jgen.writeNumberField("id", value.id);
    jgen.writeNumberField("itemNr", value.itemNr);
    jgen.writeNumberField("createdBy", value.user.id);
    jgen.writeEndObject();
}

}

Serializo el JSON con este código de Jackson How-to: Custom Serializers :

ObjectMapper mapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule("SimpleModule", 
                                              new Version(1,0,0,null));
simpleModule.addSerializer(new ItemSerializer());
mapper.registerModule(simpleModule);
StringWriter writer = new StringWriter();
try {
    mapper.writeValue(writer, myItem);
} catch (JsonGenerationException e) {
    e.printStackTrace();
} catch (JsonMappingException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

Pero me sale este error:

Exception in thread "main" java.lang.IllegalArgumentException: JsonSerializer of type com.example.ItemSerializer does not define valid handledType() (use alternative registration method?)
    at org.codehaus.jackson.map.module.SimpleSerializers.addSerializer(SimpleSerializers.java:62)
    at org.codehaus.jackson.map.module.SimpleModule.addSerializer(SimpleModule.java:54)
    at com.example.JsonTest.main(JsonTest.java:54)

¿Cómo puedo usar un serializador personalizado con Jackson?


Así es como lo haría con Gson:

public class UserAdapter implements JsonSerializer<User> {

    @Override 
    public JsonElement serialize(User src, java.lang.reflect.Type typeOfSrc,
            JsonSerializationContext context) {
        return new JsonPrimitive(src.id);
    }
}

    GsonBuilder builder = new GsonBuilder();
    builder.registerTypeAdapter(User.class, new UserAdapter());
    Gson gson = builder.create();
    String json = gson.toJson(myItem);
    System.out.println("JSON: "+json);

Pero necesito hacerlo con Jackson ahora, ya que Gson no tiene soporte para interfaces.

Jonas
fuente
¿Cómo / dónde conseguiste que Jackson usara tu serializador personalizado para el Item? Tengo un problema en el que mi método de controlador devuelve un objeto serializado estándar TypeA, pero para otro método de controlador específico, quiero serializarlo de manera diferente. Como se veria eso?
Don Cheadle
Escribí una publicación sobre Cómo escribir un serializador personalizado con Jackson que puede ser útil para algunos.
Sam Berry

Respuestas:

51

Como se mencionó, @JsonValue es una buena manera. Pero si no le importa un serializador personalizado, no es necesario escribir uno para el elemento, sino uno para el usuario; si es así, sería tan simple como:

public void serialize(Item value, JsonGenerator jgen,
    SerializerProvider provider) throws IOException,
    JsonProcessingException {
  jgen.writeNumber(id);
}

Otra posibilidad más es implementar JsonSerializable, en cuyo caso no es necesario registrarse.

En cuanto al error; eso es extraño, probablemente quieras actualizar a una versión posterior. Pero también es más seguro extenderlo, org.codehaus.jackson.map.ser.SerializerBaseya que tendrá implementaciones estándar de métodos no esenciales (es decir, todo menos la llamada de serialización real).

StaxMan
fuente
Con esto me sale el mismo error:Exception in thread "main" java.lang.IllegalArgumentException: JsonSerializer of type com.example.JsonTest$UserSerilizer does not define valid handledType() (use alternative registration method?) at org.codehaus.jackson.map.module.SimpleSerializers.addSerializer(SimpleSerializers.java:62) at org.codehaus.jackson.map.module.SimpleModule.addSerializer(SimpleModule.java:54) at com.example.JsonTest.<init>(JsonTest.java:27) at com.exampple.JsonTest.main(JsonTest.java:102)
Jonas
Utilizo la última versión estable de Jacskson, 1.8.5.
Jonas
4
Gracias. Echaré un vistazo ... ¡Ah! En realidad, es simple (aunque el mensaje de error no es bueno): solo necesita registrar el serializador con un método diferente, para especificar la clase para la que es el serializador: si no, debe devolver la clase de handleType (). Entonces use 'addSerializer' que toma JavaType o Class como argumento y debería funcionar.
StaxMan
¿Qué pasa si esto no se está ejecutando?
Matej J
62

Puede colocar @JsonSerialize(using = CustomDateSerializer.class)cualquier campo de fecha del objeto a serializar.

public class CustomDateSerializer extends SerializerBase<Date> {

    public CustomDateSerializer() {
        super(Date.class, true);
    }

    @Override
    public void serialize(Date value, JsonGenerator jgen, SerializerProvider provider)
        throws IOException, JsonProcessingException {
        SimpleDateFormat formatter = new SimpleDateFormat("EEE MMM dd yyyy HH:mm:ss 'GMT'ZZZ (z)");
        String format = formatter.format(value);
        jgen.writeString(format);
    }

}
Moesio
fuente
1
digno de mención: usar @JsonSerialize(contentUsing= ...)al anotar colecciones (por ejemplo @JsonSerialize(contentUsing= CustomDateSerializer.class) List<Date> dates)
coderatchet
33

Intenté hacer esto también, y hay un error en el código de ejemplo en la página web de Jackson que no incluye el tipo ( .class) en la llamada al addSerializer()método, que debería leerse así:

simpleModule.addSerializer(Item.class, new ItemSerializer());

En otras palabras, estas son las líneas que instancian simpleModuley agregan el serializador (con la línea incorrecta anterior comentada):

ObjectMapper mapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule("SimpleModule", 
                                          new Version(1,0,0,null));
// simpleModule.addSerializer(new ItemSerializer());
simpleModule.addSerializer(Item.class, new ItemSerializer());
mapper.registerModule(simpleModule);

FYI: Aquí está la referencia del código de ejemplo correcto: http://wiki.fasterxml.com/JacksonFeatureModules

pmhargis
fuente
9

Utilice @JsonValue:

public class User {
    int id;
    String name;

    @JsonValue
    public int getId() {
        return id;
    }
}

@JsonValue solo funciona en métodos, por lo que debe agregar el método getId. Debería poder omitir su serializador personalizado por completo.

henrik_lundgren
fuente
2
Creo que esto afectará todos los intentos de serializar un usuario, lo que hará que sea difícil exponer el nombre de un usuario en JSON.
Paul M
No puedo usar esta solución, porque también necesito poder serializar todos los objetos de usuario con todos los campos. Y esta solución romperá esa serialización ya que solo se incluirá el campo id. ¿No hay forma de crear un serilizador personalizado para Jackson como lo es para Gson?
Jonas
1
¿Puede comentar por qué las vistas JSON (en mi respuesta) no coinciden con sus necesidades?
Paul M
@usuario: Puede ser una buena solución, lo estoy leyendo y lo estoy intentando.
Jonas
2
Tenga en cuenta también que puede usar @JsonSerialize (usando = MySerializer.class) para indicar la serialización específica para su propiedad (campo o captador), por lo que solo se usa para la propiedad de miembro y NO para todas las instancias de tipo.
StaxMan
8

Escribí un ejemplo para una Timestamp.classserialización / deserialización personalizada, pero puede usarlo para lo que quiera.

Al crear el mapeador de objetos, haga algo como esto:

public class JsonUtils {

    public static ObjectMapper objectMapper = null;

    static {
        objectMapper = new ObjectMapper();
        SimpleModule s = new SimpleModule();
        s.addSerializer(Timestamp.class, new TimestampSerializerTypeHandler());
        s.addDeserializer(Timestamp.class, new TimestampDeserializerTypeHandler());
        objectMapper.registerModule(s);
    };
}

por ejemplo java ee, podría inicializarlo con esto:

import java.time.LocalDateTime;

import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;

@Provider
public class JacksonConfig implements ContextResolver<ObjectMapper> {

    private final ObjectMapper objectMapper;

    public JacksonConfig() {
        objectMapper = new ObjectMapper();
        SimpleModule s = new SimpleModule();
        s.addSerializer(Timestamp.class, new TimestampSerializerTypeHandler());
        s.addDeserializer(Timestamp.class, new TimestampDeserializerTypeHandler());
        objectMapper.registerModule(s);
    };

    @Override
    public ObjectMapper getContext(Class<?> type) {
        return objectMapper;
    }
}

donde el serializador debería ser algo como esto:

import java.io.IOException;
import java.sql.Timestamp;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

public class TimestampSerializerTypeHandler extends JsonSerializer<Timestamp> {

    @Override
    public void serialize(Timestamp value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
        String stringValue = value.toString();
        if(stringValue != null && !stringValue.isEmpty() && !stringValue.equals("null")) {
            jgen.writeString(stringValue);
        } else {
            jgen.writeNull();
        }
    }

    @Override
    public Class<Timestamp> handledType() {
        return Timestamp.class;
    }
}

y deserializador algo como esto:

import java.io.IOException;
import java.sql.Timestamp;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.SerializerProvider;

public class TimestampDeserializerTypeHandler extends JsonDeserializer<Timestamp> {

    @Override
    public Timestamp deserialize(JsonParser jp, DeserializationContext ds) throws IOException, JsonProcessingException {
        SqlTimestampConverter s = new SqlTimestampConverter();
        String value = jp.getValueAsString();
        if(value != null && !value.isEmpty() && !value.equals("null"))
            return (Timestamp) s.convert(Timestamp.class, value);
        return null;
    }

    @Override
    public Class<Timestamp> handledType() {
        return Timestamp.class;
    }
}
madx
fuente
7

Estos son patrones de comportamiento que he notado al intentar comprender la serialización de Jackson.

1) Suponga que hay un objeto Aula y una clase Estudiante. He hecho todo público y definitivo para mayor facilidad.

public class Classroom {
    public final double double1 = 1234.5678;
    public final Double Double1 = 91011.1213;
    public final Student student1 = new Student();
}

public class Student {
    public final double double2 = 1920.2122;
    public final Double Double2 = 2324.2526;
}

2) Suponga que estos son los serializadores que usamos para serializar los objetos en JSON. WriteObjectField usa el propio serializador del objeto si está registrado con el asignador de objetos; si no, lo serializa como POJO. WriteNumberField acepta exclusivamente primitivas como argumentos.

public class ClassroomSerializer extends StdSerializer<Classroom> {
    public ClassroomSerializer(Class<Classroom> t) {
        super(t);
    }

    @Override
    public void serialize(Classroom value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
        jgen.writeStartObject();
        jgen.writeObjectField("double1-Object", value.double1);
        jgen.writeNumberField("double1-Number", value.double1);
        jgen.writeObjectField("Double1-Object", value.Double1);
        jgen.writeNumberField("Double1-Number", value.Double1);
        jgen.writeObjectField("student1", value.student1);
        jgen.writeEndObject();
    }
}

public class StudentSerializer extends StdSerializer<Student> {
    public StudentSerializer(Class<Student> t) {
        super(t);
    }

    @Override
    public void serialize(Student value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
        jgen.writeStartObject();
        jgen.writeObjectField("double2-Object", value.double2);
        jgen.writeNumberField("double2-Number", value.double2);
        jgen.writeObjectField("Double2-Object", value.Double2);
        jgen.writeNumberField("Double2-Number", value.Double2);
        jgen.writeEndObject();
    }
}

3) Registre solo un DoubleSerializer con patrón de salida DecimalFormat ###,##0.000, en SimpleModule y la salida es:

{
  "double1" : 1234.5678,
  "Double1" : {
    "value" : "91,011.121"
  },
  "student1" : {
    "double2" : 1920.2122,
    "Double2" : {
      "value" : "2,324.253"
    }
  }
}

Puede ver que la serialización POJO diferencia entre double y Double, usando DoubleSerialzer para Dobles y usando un formato de Cadena regular para dobles.

4) Registre DoubleSerializer y ClassroomSerializer, sin StudentSerializer. Esperamos que la salida sea tal que si escribimos un doble como un objeto, se comportará como un Doble, y si escribimos un Doble como un número, se comportará como un doble. La variable de instancia Student debe escribirse como POJO y seguir el patrón anterior, ya que no se registra.

{
  "double1-Object" : {
    "value" : "1,234.568"
  },
  "double1-Number" : 1234.5678,
  "Double1-Object" : {
    "value" : "91,011.121"
  },
  "Double1-Number" : 91011.1213,
  "student1" : {
    "double2" : 1920.2122,
    "Double2" : {
      "value" : "2,324.253"
    }
  }
}

5) Registre todos los serializadores. La salida es:

{
  "double1-Object" : {
    "value" : "1,234.568"
  },
  "double1-Number" : 1234.5678,
  "Double1-Object" : {
    "value" : "91,011.121"
  },
  "Double1-Number" : 91011.1213,
  "student1" : {
    "double2-Object" : {
      "value" : "1,920.212"
    },
    "double2-Number" : 1920.2122,
    "Double2-Object" : {
      "value" : "2,324.253"
    },
    "Double2-Number" : 2324.2526
  }
}

exactamente como se esperaba.

Otra nota importante: si tiene varios serializadores para la misma clase registrados con el mismo módulo, entonces el módulo seleccionará el serializador para esa clase que se agregó más recientemente a la lista. Esto no debe usarse, es confuso y no estoy seguro de cuán consistente es esto

Moraleja: si desea personalizar la serialización de primitivas en su objeto, debe escribir su propio serializador para el objeto. No puede confiar en la serialización de POJO Jackson.

hombre riendo
fuente
¿Cómo se registra ClassroomSerializer para manejar, por ejemplo, las incidencias de Classroom?
Trismegistos
5

Las vistas JSON de Jackson pueden ser una forma más sencilla de lograr sus requisitos, especialmente si tiene cierta flexibilidad en su formato JSON.

Si {"id":7, "itemNr":"TEST", "createdBy":{id:3}}es una representación aceptable, será muy fácil de lograr con muy poco código.

Simplemente anotaría el campo de nombre de Usuario como parte de una vista y especificaría una vista diferente en su solicitud de serialización (los campos sin anotaciones se incluirían de forma predeterminada)

Por ejemplo: Defina las vistas:

public class Views {
    public static class BasicView{}
    public static class CompleteUserView{}
}

Anotar al usuario:

public class User {
    public final int id;

    @JsonView(Views.CompleteUserView.class)
    public final String name;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

Y serialice solicitando una vista que no contenga el campo que desea ocultar (los campos no anotados se serializan por defecto):

objectMapper.getSerializationConfig().withView(Views.BasicView.class);
Paul M
fuente
Encuentro que Jackson JSON Views es difícil de usar y no puedo encontrar una buena solución para este problema.
Jonas
Jonas: he añadido un ejemplo. Encontré que las vistas son una solución realmente agradable para serializar el mismo objeto de diferentes maneras.
Paul M
Gracias por un buen ejemplo. Esta es la mejor solución hasta ahora. Pero, ¿no hay forma de obtener createdByun valor en lugar de un objeto?
Jonas
setSerializationView()parece estar en desuso, así que usé en su mapper.viewWriter(JacksonViews.ItemView.class).writeValue(writer, myItem);lugar.
Jonas
Lo dudo usando jsonviews. Una solución rápida y sucia que utilicé antes de descubrir las vistas era simplemente copiar las propiedades que me interesaban en un mapa y luego serializar el mapa.
Paul M
5

En mi caso (Spring 3.2.4 y Jackson 2.3.1), configuración XML para serializador personalizado:

<mvc:annotation-driven>
    <mvc:message-converters register-defaults="false">
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
            <property name="objectMapper">
                <bean class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
                    <property name="serializers">
                        <array>
                            <bean class="com.example.business.serializer.json.CustomObjectSerializer"/>
                        </array>
                    </property>
                </bean>
            </property>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

se sobrescribió de forma inexplicable de nuevo al valor predeterminado por algo.

Esto funcionó para mí:

CustomObject.java

@JsonSerialize(using = CustomObjectSerializer.class)
public class CustomObject {

    private Long value;

    public Long getValue() {
        return value;
    }

    public void setValue(Long value) {
        this.value = value;
    }
}

CustomObjectSerializer.java

public class CustomObjectSerializer extends JsonSerializer<CustomObject> {

    @Override
    public void serialize(CustomObject value, JsonGenerator jgen,
        SerializerProvider provider) throws IOException,JsonProcessingException {
        jgen.writeStartObject();
        jgen.writeNumberField("y", value.getValue());
        jgen.writeEndObject();
    }

    @Override
    public Class<CustomObject> handledType() {
        return CustomObject.class;
    }
}

No <mvc:message-converters>(...)</mvc:message-converters>se necesita ninguna configuración XML ( ) en mi solución.

usuario11153
fuente
1

Si su único requisito en su serializador personalizado es omitir la serialización del namecampo de User, márquelo como transitorio . Jackson no serializará ni deserializará transitorio campos .

[ver también: ¿Por qué Java tiene campos transitorios? ]

Mike G
fuente
¿Dónde lo marco? ¿En la Userclase? Pero también serializaré todos los objetos de usuario. Por ejemplo, primero solo serialice todo items(con solo userIdcomo referencia al objeto de usuario) y luego serialice todo users. En este caso, no puedo marcar los campos en la Userclase.
Jonas
A la luz de esta nueva información, este enfoque no funcionará para usted. Parece que Jackson está buscando más información para el serializador personalizado (¿el método handleType () necesita anularse?)
Mike G
Sí, pero no hay nada sobre el handledType()método en la documentación a la que me vinculé y cuando Eclipse genera los métodos para implementar no handledType()se genera, así que estoy confundido.
Jonas
No estoy seguro porque el wiki que vinculó no hace referencia a él, pero en la versión 1.5.1 hay un handleType () y la excepción parece estar quejándose de que el método falta o es inválido (la clase base devuelve nulo del método). jackson.codehaus.org/1.5.1/javadoc/org/codehaus/jackson/map/…
Mike G
1

Tienes que anular el método handleType y todo funcionará

@Override
public Class<Item> handledType()
{
  return Item.class;
}
Sergey Kukurudzyak
fuente
0

El problema en su caso es que al ItemSerializer le falta el método handleType () que debe anularse desde JsonSerializer

    public class ItemSerializer extends JsonSerializer<Item> {

    @Override
    public void serialize(Item value, JsonGenerator jgen,
            SerializerProvider provider) throws IOException,
            JsonProcessingException {
        jgen.writeStartObject();
        jgen.writeNumberField("id", value.id);
        jgen.writeNumberField("itemNr", value.itemNr);
        jgen.writeNumberField("createdBy", value.user.id);
        jgen.writeEndObject();
    }

   @Override
   public Class<Item> handledType()
   {
    return Item.class;
   }
}

Por lo tanto, está obteniendo el error explícito de que no está definido handleType ()

Exception in thread "main" java.lang.IllegalArgumentException: JsonSerializer of type com.example.ItemSerializer does not define valid handledType() 

Espero que ayude a alguien. Gracias por leer mi respuesta.

Sanjay Bharwani
fuente