Como faço para usar um serializador personalizado com Jackson?

111

Tenho duas classes Java que desejo serializar para 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;
    }
}

Quero serializar um item para este JSON:

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

com o usuário serializado para incluir apenas o id. Também poderei serilizar todos os objetos de usuário para JSON como:

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

Então, acho que preciso escrever um serializador personalizado Iteme tentei com isso:

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

}

Eu serializo o JSON com 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();
}

Mas recebo este erro:

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)

Como posso usar um serializador personalizado com Jackson?


É assim que eu faria com 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);

Mas preciso fazer isso com Jackson agora, já que Gson não tem suporte para interfaces.

Jonas
fonte
como / onde você conseguiu que Jackson usasse seu serializador personalizado para o Item? Estou tendo um problema em que meu método de controlador retorna um objeto serializado padrão TypeA, mas para outro método de controlador específico, quero serializá-lo de maneira diferente. Qual seria a aparência disso?
Don Cheadle
Eu escrevi um post sobre como escrever um serializador personalizado com Jackson que pode ser útil para alguns.
Sam Berry

Respostas:

51

Como mencionado, @JsonValue é uma boa maneira. Mas se você não se importa com um serializador personalizado, não há necessidade de escrever um para o Item, mas sim um para o Usuário - em caso afirmativo, seria tão simples como:

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

Ainda outra possibilidade é implementar JsonSerializable, caso em que nenhum registro é necessário.

Quanto ao erro; isso é estranho - você provavelmente deseja atualizar para uma versão posterior. Mas também é mais seguro estender org.codehaus.jackson.map.ser.SerializerBase, pois terá implementações padrão de métodos não essenciais (ou seja, tudo, exceto a chamada de serialização real).

StaxMan
fonte
Com isso recebo o mesmo erro: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
Eu uso a última versão estável do Jacskson, 1.8.5.
Jonas
4
Obrigado. Vou dar uma olhada ... Ah! Na verdade, é simples (embora a mensagem de erro não seja boa) - você só precisa registrar o serializador com um método diferente, para especificar a classe para a qual o serializador se destina: do contrário, ele deve retornar a classe de handledType (). Portanto, use 'addSerializer' que leva JavaType ou Class como argumento e deve funcionar.
StaxMan
E se isso não estiver sendo executado?
Matej J
62

Você pode colocar @JsonSerialize(using = CustomDateSerializer.class)qualquer campo de data do objeto a ser serializado.

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
fonte
1
digno de nota: use @JsonSerialize(contentUsing= ...)ao anotar Coleções (por exemplo @JsonSerialize(contentUsing= CustomDateSerializer.class) List<Date> dates)
coderatchet
33

Eu tentei fazer isso também, e há um erro no código de exemplo na página da Web de Jackson que falha ao incluir o type ( .class) na chamada do addSerializer()método, que deveria ser assim:

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

Em outras palavras, estas são as linhas que instanciam o simpleModulee adicionam o serializador (com a linha incorreta 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);

Para sua informação: aqui está a referência para o código de exemplo correto: http://wiki.fasterxml.com/JacksonFeatureModules

pmhargis
fonte
9

Use @JsonValue:

public class User {
    int id;
    String name;

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

@JsonValue funciona apenas em métodos, portanto, você deve adicionar o método getId. Você deve ser capaz de ignorar o serializador personalizado completamente.

Henrik_lundgren
fonte
2
Acho que isso afetará todas as tentativas de serializar um usuário, dificultando a exposição do nome de um usuário em JSON.
Paul M
Não posso usar esta solução, porque também preciso ser capaz de serializar todos os objetos de usuário com todos os campos. E essa solução vai quebrar essa serialização, pois apenas o campo id será incluído. Não há como criar um serilizador personalizado para Jackson como é para Gson?
Jonas
1
Você pode comentar por que JSON Views (em minha resposta) não correspondem às suas necessidades?
Paul M
@ usuário: Pode ser uma boa solução, estou lendo sobre isso e tentando.
Jonas
2
Observe também que você pode usar @JsonSerialize (using = MySerializer.class) para indicar serialização específica para sua propriedade (campo ou getter), portanto, é usado apenas para propriedade de membro e NÃO todas as instâncias do tipo.
StaxMan de
8

Escrevi um exemplo de Timestamp.classserialização / desserialização customizada , mas você pode usá-lo para o que quiser.

Ao criar o mapeador de objetos, faça algo assim:

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 exemplo, java eevocê poderia inicializá-lo com este:

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

onde o serializador deve ser algo assim:

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

e desserializador algo assim:

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
fonte
7

Esses são padrões de comportamento que observei ao tentar entender a serialização de Jackson.

1) Suponha que haja um objeto Sala de Aula e uma classe Aluno. Tornei tudo público e definitivo para facilitar.

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) Suponha que esses sejam os serializadores que usamos para serializar os objetos em JSON. O writeObjectField usa o próprio serializador do objeto se ele estiver registrado com o mapeador de objeto; caso contrário, ele o serializa como um POJO. O writeNumberField aceita exclusivamente primitivos 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 apenas um DoubleSerializer com o padrão de saída DecimalFormat ###,##0.000, em SimpleModule e a saída será:

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

Você pode ver que a serialização POJO diferencia entre double e Double, usando o DoubleSerialzer para Doubles e usando um formato de String regular para doubles.

4) Registre DoubleSerializer e ClassroomSerializer, sem o StudentSerializer. Esperamos que a saída seja tal que, se escrevermos um duplo como um objeto, ele se comporte como um duplo, e se escrevermos um duplo como um número, ele se comportará como um duplo. A variável de instância Student deve ser escrita como um POJO e seguir o padrão acima, uma vez que não é registrada.

{
  "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 os serializadores. O resultado é:

{
  "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
  }
}

exatamente como esperado.

Outra observação importante: se você tiver vários serializadores para a mesma classe registrados com o mesmo Módulo, o Módulo selecionará o serializador para essa classe que foi adicionado mais recentemente à lista. Isso não deve ser usado - é confuso e não tenho certeza de quão consistente é

Moral: se você deseja personalizar a serialização de primitivas em seu objeto, deve escrever seu próprio serializador para o objeto. Você não pode confiar na serialização do POJO Jackson.

homem rindo
fonte
Como você registra ClassroomSerializer para lidar, por exemplo, com ocorrências de Classroom?
Trismegistos
5

As visualizações JSON de Jackson podem ser uma maneira mais simples de atingir seus requisitos, especialmente se você tiver alguma flexibilidade em seu formato JSON.

Se {"id":7, "itemNr":"TEST", "createdBy":{id:3}}for uma representação aceitável, isso será muito fácil de conseguir com muito pouco código.

Você apenas anota o campo de nome do usuário como sendo parte de uma visualização e especifica uma visualização diferente em sua solicitação de serialização (os campos não anotados seriam incluídos por padrão)

Por exemplo: Defina as visualizações:

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

Anote o usuário:

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

E serializar solicitando uma visualização que não contenha o campo que você deseja ocultar (campos não anotados são serializados por padrão):

objectMapper.getSerializationConfig().withView(Views.BasicView.class);
Paul M
fonte
Acho o Jackson JSON Views difícil de usar e não consigo encontrar uma boa solução para esse problema.
Jonas
Jonas - Adicionei um exemplo. Achei as visualizações uma solução muito boa para serializar o mesmo objeto de maneiras diferentes.
Paul M
Obrigado por um bom exemplo. Esta é a melhor solução até agora. Mas não há como obter createdByum valor em vez de um objeto?
Jonas
setSerializationView()parece estar obsoleto, então usei mapper.viewWriter(JacksonViews.ItemView.class).writeValue(writer, myItem);.
Jonas
Eu duvido usando jsonviews. Uma solução rápida e suja que usei antes de descobrir visualizações foi apenas copiar as propriedades nas quais estava interessado em um mapa e, em seguida, serializar o mapa.
Paul M
5

No meu caso (Spring 3.2.4 e Jackson 2.3.1), configuração 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>

foi de forma inexplicável substituída de volta ao padrão por algo.

Isso funcionou para mim:

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

Nenhuma configuração XML ( <mvc:message-converters>(...)</mvc:message-converters>) é necessária em minha solução.

user11153
fonte
1

Se o seu único requisito em seu serializador personalizado é ignorar a serialização do namecampo de User, marque-o como temporário . Jackson não serializará ou desserializará campos transientes .

[veja também: Por que o Java possui campos transitórios? ]

Mike G
fonte
Onde posso marcá-lo? Na User-classe? Mas vou serializar todos os objetos do usuário também. Por exemplo, primeiro apenas serializar todos items(apenas userIdcomo uma referência ao objeto de usuário) e, em seguida, serializar todos users. Neste caso, não posso marcar os campos na Userclasse -class.
Jonas
À luz dessas novas informações, essa abordagem não funcionará para você. Parece que Jackson está procurando mais informações para o serializador personalizado (método handledType () precisa ser substituído?)
Mike G
Sim, mas não há nada sobre o handledType()método na documentação que vinculei e quando o Eclipse gera os métodos para implementar não handledType()é gerado, então estou confuso.
Jonas
Não tenho certeza porque o wiki que você vinculou não faz referência a ele, mas na versão 1.5.1 há um handledType () e a exceção parece estar reclamando que o método está ausente ou é inválido (a classe base retorna nulo do método). jackson.codehaus.org/1.5.1/javadoc/org/codehaus/jackson/map/…
Mike G
1

Você tem que substituir o método handledType e tudo funcionará

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

O problema no seu caso é que o ItemSerializer está sem o método handledType () que precisa ser substituído pelo 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;
   }
}

Portanto, você está recebendo o erro explícito de que handledType () não está definido

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

Espero que ajude alguém. Obrigado por ler minha resposta.

Sanjay Bharwani
fonte