Documentation for a newer release is available. View Latest

Serialización

La serialización en IPF se realiza con Jackson, y su propia documentación cubrirá mucho más de lo que pretende esta página, y existen innumerables tutoriales y recursos en línea que pueden ayudar a comprender y usar Jackson. Un buen lugar para comenzar es la documentación oficial.

El objetivo de esta página es una breve introducción a la serialización dentro de IPF, incluyendo el comportamiento predeterminado y cómo puede personalizarse, así como algunas consideraciones y recomendaciones.

Conceptos

Esta sección pretende ser breve. Todos los conceptos enumerados aquí se tratan con más detalle en secciones posteriores.

SerializationHelper

SerializationHelper es el punto de entrada principal para la serialización. Proporciona métodos de conveniencia para serializar a y deserializar desde JSON, y en última instancia delega en su instancia subyacente singleton de Jackson com.fasterxml.jackson.databind.ObjectMapper.

La instancia singleton subyacente de ObjectMapper se crea utilizando ObjectMapperFactory.

Ejemplos

//Accede a la instancia singleton de ObjectMapper
ObjectMapper objectMapper = SerializationHelper.objectMapper();

//p. ej., para convertir entre tipos
objectMapper.convertValue(object, SomeOtherType.class);
//Métodos de conveniencia para serializar a JSON usando el ObjectMapper singleton
String json = SerializationHelper.objectToString(object);

//Y para deserializar desde JSON
MyObject deserialized = SerializationHelper.stringToObject(json, MyObject.class);

ObjectMapperFactory

com.iconsolutions.ipf.core.shared.api.serializer.ObjectMapperFactory es el punto de entrada principal para crear nuevas instancias de ObjectMapper.

Si necesita una instancia diferente a la proporcionada por SerializationHelper.objectMapper(), usaría esta clase. SerializationHelper usa ObjectMapperFactory para crear su propia instancia singleton de ObjectMapper.

Ejemplos

//Crea una nueva instancia de ObjectMapper que se comporta igual que SerializationHelper.objectMapper()
ObjectMapper serializationHelperObjectMapper = ObjectMapperFactory.createObjectMapper();

//Crea una nueva instancia de ObjectMapper con configuración adicional
ObjectMapper customSerializationHelperObjectMapper = ObjectMapperFactory.createObjectMapper(config);

//Crea una nueva instancia de ObjectMapper con nombre que debe configurarse por separado
ObjectMapper customObjectMapper = ObjectMapperFactory.createObjectMapper("my-object-mapper", config);

Akka Jackson

Las instancias de ObjectMapper se configuran utilizando Akka Jackson, lo que significa que el comportamiento de serialización puede personalizarse con configuración Hocon. Se incluye configuración predeterminada para un serializador llamado serialization-helper, y este serializador puede personalizarse con configuración adicional.

akka.serialization.jackson.serialization-helper {
  //Se pueden agregar módulos adicionales para admitir comportamiento de (de)serialización personalizado
  jackson-modules += "MyCustomModule"
}

También se pueden definir nuevos serializadores, lo cual es una buena manera de personalizar la serialización sin cambiar el comportamiento de serialization-helper.

akka.serialization.jackson.my-serializer = "akka.serialization.jackson.JacksonJsonSerializer"

//El serializador personalizado puede heredar el comportamiento predeterminado y proporcionar personalización adicional
akka.serialization.jackson.my-serializer = ${akka.serialization.jackson.serialization-helper} {
  jackson-modules += "MyCustomModule"
}

Primeros pasos

Usar el SerializationHelper predeterminado

En muchos casos Comportamiento de serialización predeterminado será suficiente, lo que significa que la serialización puede realizarse usando los métodos de conveniencia en SerializationHelper, o accediendo a la instancia subyacente de ObjectMapper.

String json = SerializationHelper.objectToString(object);
MyObject deserialized = SerializationHelper.stringToObject(json, MyObject.class);

Map<String, Object> b = SerializationHelper.objectMapper().convertValue(deserialized, new TypeReference<Map<String, Object>>() {
});

Personalizar el SerializationHelper predeterminado

Por supuesto, habrá casos en los que el comportamiento de serialización predeterminado no sea suficiente, p. ej., cuando los tipos deban serializarse de manera personalizada.

La personalización idealmente debe hacerse mediante configuración. Consulte la Referencia de configuración para el conjunto completo de opciones de configuración. Evite configurar/mutar programáticamente la instancia predeterminada de ObjectMapper.

Registrar módulos personalizados

Dado el siguiente módulo personalizado

public final class MyModule extends SimpleModule {
    @Override
    public void setupModule(final SetupContext context) {
        super.setupModule(context);

        final var serializers = new SimpleSerializers();
        serializers.addSerializer(SomeType.class, new JsonSerializer<>() {
            @Override
            public void serialize(final SomeType value, final JsonGenerator gen, final SerializerProvider serializers) {
                //Serialización personalizada para SomeType
            }
        });
        context.addSerializers(serializers);

        final var deserializers = new SimpleDeserializers();
        deserializers.addDeserializer(SomeType.class, new JsonDeserializer<>() {
            @Override
            public SomeType deserialize(final JsonParser p, final DeserializationContext ctxt) {
                //Deserialización personalizada para SomeType
            }
        });
        context.addDeserializers(deserializers);
    }

    record SomeType(String name) {
    }
}

El módulo se puede agregar al serializador serialization-helper con la siguiente configuración:

akka.serialization.jackson.serialization-helper.jackson-modules += "package.of.my.module.MyModule"

Consideraciones

Cambiar el comportamiento de serialización predeterminado debe abordarse con precaución y probablemente sea mejor evitarlo. En la mayoría de los casos, agregar tipos admitidos personalizados a través de módulos estará bien, pero cambiar el comportamiento de serialización predeterminado afectará a todo el código que usa la serialización predeterminada. Cuando se requiera configuración adicional, considere en su lugar Crear serializadores personalizados.

La configuración programática, es decir, mutar la instancia de ObjectMapper, debe evitarse porque depende de que se invoque esa ruta de código para que la configuración surta efecto, p. ej., evite hacer lo siguiente

SerializationHelper.objectMapper().registerModule(new MyModule())

Usar la serialización IPF en una aplicación Spring

Las aplicaciones web Spring suelen configurar su propio bean ObjectMapper, y Spring viene con sus propios mecanismos para personalizar la instancia ObjectMapper.

Puede preferir usar el comportamiento de serialización de IPF. Para hacerlo, dependa de ipf-serialization-autoconfigure.

<dependency>
    <groupId>com.iconsolutions.ipf.core.platform</groupId>
    <artifactId>ipf-serialization-autoconfigure</artifactId>
</dependency>

Este módulo de autoconfiguración de Spring se configura antes de cualquier autoconfiguración de Jackson de Spring, proporcionando el mismo ObjectMapper predeterminado (es una instancia diferente) usado por SerializationHelper.

Crear serializadores personalizados

Crear nuevos serializadores, lo cual será necesario cuando el comportamiento de serialización predeterminado no sea suficiente, puede lograrse de diferentes maneras. Normalmente los proyectos downstream crean su propia instancia de ObjectMapper, la configuran programáticamente y ponen esa instancia a disposición en todos los lugares donde se necesita, pero también es posible definir y configurar serializadores personalizados vía hocon.

El enfoque elegido puede depender de los requisitos, p. ej., si necesita el comportamiento predeterminado con pequeños cambios, o si necesita un ObjectMapper completamente nuevo sin los valores predeterminados de IPF.

Cuando los valores predeterminados casi son suficientes, pero necesita cambiar cómo se serializan los valores nulos y vacíos, podría crear programáticamente una nueva instancia predeterminada y sobrescribir las inclusiones de serialización, p. ej.

//Crea una nueva instancia del ObjectMapper predeterminado de serialización
var mapper ObjectMapperFactory.createObjectMapper();

//Y la sobrescribe para escribir siempre nulos y objetos vacíos al serializar a JSON
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);

Alternativamente, puede definir un mapper personalizado con sus propios módulos para personalización, p. ej.

akka {
  serializers.my-custom-mapper = "akka.serialization.jackson.JacksonJsonSerializer"

  //Hereda de serialization-helper, pero con personalizaciones
  serialization.jackson.my-custom-mapper = ${akka.serialization.jackson.serialization-helper} {
    jackson-modules += "com.food.bar.MyCustomModule"
  }
}
/**
 * Sobrescribe la configuración de inclusión de serialización
 */
public final class MyCustomModule extends SimpleModule {

    @Override
    public void setupModule(final SetupContext context) {
        super.setupModule(context);
        final ObjectMapper objectMapper = context.getOwner();
        objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
    }
}
var mapper = ObjectMapperFactory.createObjectMapper("my-custom-mapper", config);

Cuando se requiera un serializador completamente nuevo, omita el marcador de posición usado para heredar de serialization-helper, p. ej.

akka {
  serializers.my-custom-mapper = "akka.serialization.jackson.JacksonJsonSerializer"
}
Cuando la serialización deba compartirse, p. ej., si produce una aplicación cliente y servidor, considere introducir un módulo de serialización compartido con su propia configuración/configuración programática y clases/métodos auxiliares, y por supuesto, pruebas, en lugar de duplicar la configuración y la inicialización del serializador.

Comportamiento de serialización predeterminado

La instancia ObjectMapper de serialization-helper predeterminada es una instancia personalizada de lo definido para la serialización Akka Jackson, p. ej., configuración bajo akka.serialization.jackson, que a su vez es una instancia personalizada del ObjectMapper predeterminado, p. ej., new ObjectMapper().

NOTA: Se puede suponer que todas las características y configuraciones no mencionadas aquí son los valores predeterminados de Jackson.

Características de serialización

Característica Activada/Desactivada Descripción

WRITE_DURATIONS_AS_TIMESTAMPS

Activada

La duración java.time.Duration.ofHours(5) se serializa como {"duration":18000.000000000}, en lugar de {"duration":"PT5H"}, p. ej.

FAIL_ON_EMPTY_BEANS

Activada

Se lanzará una excepción al intentar serializar un objeto que no tenga propiedades serializables.

Características de deserialización

Característica Activada/Desactivada Descripción

USE_BIG_DECIMAL_FOR_FLOATS

Activada

Establece el tipo de dato usado al deserializar números de punto flotante, en este caso java.math.BigDecimal.

Módulos

Los siguientes módulos se incluyen en el serializador predeterminado.

Módulo Descripción

DefaultScalaModule

Un módulo de Jackson que añade soporte para tipos de Scala

ParameterNamesModule

Un módulo de Jackson que añade soporte para acceder a nombres de parámetros; una característica añadida en JDK 8.

Jdk8Module

Un módulo de Jackson que añade soporte para tipos de jdk8, p. ej. java.util.Optional.

DateTimeModule

Un módulo de IPF que añade soporte para tipos de tiempo de Java, p. ej. java.time.Instant, pero con personalizaciones específicas de IPF.

OpenApiGeneratorSupportModule

Un módulo de IPF que compensa un problema con tipos enum con métodos de fábrica @JsonCreator incompletos producidos por openapi-generator. Este módulo puede eliminarse en una versión futura.

DomElementDeserializationModule

Un módulo de IPF que añade soporte para elementos DOM

UntypedObjectDeserializationModule

Un módulo de IPF que añade soporte para deserializar tipos java.lang.Object sin tipo.

Referencia de configuración

Se utiliza Akka Jackson para configurar los serializadores, por lo que el mejor lugar para comenzar es la documentación oficial. Para opciones de configuración específicas, consulte la sección Características adicionales.

Los detalles de configuración en la sección de Características adicionales, p. ej., todo lo que está bajo akka.serialization.jackson también puede aplicarse al serialization-helper predeterminado akka.serialization.jackson.serialization-helper, y a los serializadores personalizados, p. ej., akka.serialization.jackson.my-custom-serializer.

Akka Jackson no admite configurar las inclusiones de serialización para la instancia ObjectMapper, y normalmente esto se hace programáticamente, p. ej., new ObjectMapper().setSerializationInclusion(JsonIncludes.NON_NUll), pero esto puede lograrse con un módulo personalizado que personalice la instancia subyacente de ObjectMapper, p. ej.

public final class JsonIncludeOverrideModule extends SimpleModule {

    @Override
    public void setupModule(final SetupContext context) {
        super.setupModule(context);
        final ObjectMapper objectMapper = context.getOwner();
        objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
    }
}

Qué hacer, qué no hacer, sugerencias, opiniones

Soportar tipos personalizados

Evite usar SerializationHelper, o cualquier otra instancia de ObjectMapper dentro de implementaciones personalizadas de com.fasterxml.jackson.databind.JsonSerializer y com.fasterxml.jackson.databind.JsonDeserializer, p. ej., no haga

final class SomeOtherObjectJsonSerializer extends JsonSerializer<SomeOtherObject> {
    @Override
    public void serialize(final SomeOtherObject value, final JsonGenerator gen, final SerializerProvider serializers) throws IOException {
        //Usar SerializationHelper
        final var string = SerializationHelper.objectToString(value);
        gen.writeString(string);
    }
}

Este JsonSerializer personalizado puede usarse dentro de cualquier instancia personalizada de ObjectMapper, pero depende del ObjectMapper predeterminado de serialization-helper, que puede comportarse de manera diferente en tiempo de ejecución, dependiendo de la configuración.

Prefiera el mecanismo proporcionado, p. ej., en el serializador anterior probablemente pueda lograr lo que necesita con los argumentos JsonGenerator y SerializerProvider. El JsonDeserializer proporciona facilidades similares.

Compartir serialización personalizada

Si tiene requisitos específicos de serialización y el serialization-helper predeterminado no es suficiente, considere crear su propio serializador con nombre y colocar su configuración y clases/métodos auxiliares, y por supuesto, pruebas, en un nuevo módulo. Este módulo puede ser usado por otros proyectos downstream.

Evitar personalizar SerializationHelper

No mutar la instancia singleton de ObjectMapper, y evite cualquier configuración más allá del soporte para tipos adicionales. Si necesita algo más específico, cree uno nuevo.