Come si usa un serializzatore personalizzato con Jackson?


111

Ho due classi Java che voglio serializzare in 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;
    }
}

Voglio serializzare un articolo in questo JSON:

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

con User serializzato per includere solo il file id. Potrò anche serilizzare tutti gli oggetti utente in JSON come:

{"id":3, "name": "Jonas", "email": "jonas@example.com"}

Quindi immagino di dover scrivere un serializzatore personalizzato per Item e provato con questo:

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

}

Serializzo il JSON con questo codice da Jackson How-to: Serializzatori personalizzati :

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

Ma ottengo questo errore:

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)

Come posso utilizzare un serializzatore personalizzato con Jackson?


Ecco come lo farei 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);

Ma ora devo farlo con Jackson, poiché Gson non ha il supporto per le interfacce.


come / dove hai convinto Jackson a usare il tuo serializzatore personalizzato per il Item? Sto riscontrando un problema in cui il mio metodo del controller restituisce un oggetto serializzato standard TypeA, ma per un altro metodo del controller specifico, voglio serializzarlo in modo diverso. Come sarebbe?
Don Cheadle,

Ho scritto un post su Come scrivere un serializzatore personalizzato con Jackson che potrebbe essere utile ad alcuni.
Sam Berry

Risposte:


51

Come accennato, @JsonValue è un buon modo. Ma se non ti dispiace un serializzatore personalizzato, non è necessario scriverne uno per Item ma piuttosto uno per Utente - in tal caso, sarebbe semplice come:

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

Un'altra possibilità è implementare JsonSerializable, nel qual caso non è necessaria la registrazione.

Quanto all'errore; è strano: probabilmente vorrai eseguire l'aggiornamento a una versione successiva. Ma è anche più sicuro estenderlo org.codehaus.jackson.map.ser.SerializerBasein quanto avrà implementazioni standard di metodi non essenziali (cioè tutto tranne la chiamata di serializzazione effettiva).


Con questo ottengo lo stesso errore: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

Uso l'ultima versione stabile di Jacskson, 1.8.5.
Jonas

4
Grazie. Darò un'occhiata ... Ah! In realtà è semplice (sebbene il messaggio di errore non sia buono): devi solo registrare il serializzatore con un metodo diverso, per specificare la classe a cui è destinato il serializzatore: in caso contrario, deve restituire la classe da handledType (). Quindi usa 'addSerializer' che accetta JavaType o Class come argomento e dovrebbe funzionare.
StaxMan

E se questo non viene eseguito?
Matej J

62

È possibile inserire @JsonSerialize(using = CustomDateSerializer.class)qualsiasi campo data dell'oggetto da serializzare.

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

}

1
degno di nota: utilizzare @JsonSerialize(contentUsing= ...)per annotare le raccolte (ad esempio @JsonSerialize(contentUsing= CustomDateSerializer.class) List<Date> dates)
coderatchet

33

Ho provato a farlo anche io, e c'è un errore nel codice di esempio sulla pagina web di Jackson che non riesce a includere il type ( .class) nella chiamata al addSerializer()metodo, che dovrebbe leggere in questo modo:

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

In altre parole, queste sono le righe che istanziano simpleModulee aggiungono il serializzatore (con la riga precedente errata commentata):

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: ecco il riferimento per il codice di esempio corretto: http://wiki.fasterxml.com/JacksonFeatureModules


9

Usa @JsonValue:

public class User {
    int id;
    String name;

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

@JsonValue funziona solo sui metodi, quindi devi aggiungere il metodo getId. Dovresti essere in grado di saltare del tutto il serializzatore personalizzato.


2
Penso che questo avrà un impatto su tutti i tentativi di serializzare un utente, rendendo difficile esporre il nome di un utente su JSON.
Paul M

Non posso usare questa soluzione, perché devo anche essere in grado di serializzare tutti gli oggetti utente con tutti i campi. E questa soluzione interromperà quella serializzazione poiché verrà incluso solo il campo id. Non c'è modo di creare un serilizzatore personalizzato per Jackson come lo è per Gson?
Jonas

1
Puoi commentare perché le viste JSON (nella mia risposta) non corrispondono alle tue esigenze?
Paul M

@user: Potrebbe essere una buona soluzione, sto leggendo e provando.
Jonas

2
Nota anche che puoi usare @JsonSerialize (usando = MySerializer.class) per indicare una serializzazione specifica per la tua proprietà (campo o getter), quindi viene usata solo per la proprietà del membro e NON per tutte le istanze di tipo.
StaxMan

8

Ho scritto un esempio per una Timestamp.classserializzazione / deserializzazione personalizzata, ma potresti usarlo per quello che vuoi.

Quando crei il mappatore di oggetti, fai qualcosa di simile:

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

per esempio in java eepotresti inizializzarlo con questo:

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

dove il serializzatore dovrebbe essere qualcosa del genere:

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 deserializzatore qualcosa del genere:

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

7

Questi sono modelli di comportamento che ho notato mentre cercavo di capire la serializzazione di Jackson.

1) Supponiamo che ci sia un oggetto Classroom e una classe Student. Ho reso tutto pubblico e definitivo per facilità.

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) Supponiamo che questi siano i serializzatori che utilizziamo per serializzare gli oggetti in JSON. WriteObjectField utilizza il serializzatore dell'oggetto se è registrato con l'oggetto mapper; in caso contrario, lo serializza come POJO. WriteNumberField accetta esclusivamente primitive come argomenti.

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) Registra solo un DoubleSerializer con pattern di output DecimalFormat ###,##0.000, in SimpleModule e l'output è:

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

Puoi vedere che la serializzazione POJO differenzia tra double e Double, utilizzando DoubleSerialzer per Double e utilizzando un formato String regolare per double.

4) Registrare DoubleSerializer e ClassroomSerializer, senza StudentSerializer. Ci aspettiamo che l'output sia tale che se scriviamo un double come oggetto, si comporti come un Double, e se scriviamo un Double come numero, si comporterà come un double. La variabile di istanza Student dovrebbe essere scritta come POJO e seguire lo schema precedente poiché non si 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) Registra tutti i serializzatori. L'output è:

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

esattamente come previsto.

Un'altra nota importante: se hai più serializzatori per la stessa classe registrati con lo stesso modulo, il modulo selezionerà il serializzatore per quella classe che è stata aggiunta più di recente all'elenco. Questo non dovrebbe essere usato - è fonte di confusione e non sono sicuro di quanto sia coerente

Morale: se vuoi personalizzare la serializzazione delle primitive nel tuo oggetto, devi scrivere il tuo serializzatore per l'oggetto. Non puoi fare affidamento sulla serializzazione POJO Jackson.


Come si registra ClassroomSerializer per gestire, ad esempio, gli eventi di Classroom?
Trismegistos

5

Le viste JSON di Jackson potrebbero essere un modo più semplice per soddisfare le tue esigenze, soprattutto se hai una certa flessibilità nel tuo formato JSON.

Se {"id":7, "itemNr":"TEST", "createdBy":{id:3}}è una rappresentazione accettabile, sarà molto facile da ottenere con pochissimo codice.

Dovresti semplicemente annotare il campo del nome dell'utente come parte di una vista e specificare una vista diversa nella richiesta di serializzazione (i campi non annotati sarebbero inclusi per impostazione predefinita)

Ad esempio: definire le visualizzazioni:

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

Annota l'utente:

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 serializzare la richiesta di una vista che non contiene il campo che si desidera nascondere (i campi non annotati sono serializzati per impostazione predefinita):

objectMapper.getSerializationConfig().withView(Views.BasicView.class);

Trovo le viste Jackson JSON difficili da usare e non riesco a trovare una buona soluzione per questo problema.
Jonas

Jonas - Ho aggiunto un esempio. Ho trovato le viste una soluzione davvero interessante per serializzare lo stesso oggetto in modi diversi.
Paul M

Grazie per un buon esempio. Questa è la migliore soluzione finora. Ma non c'è modo di ottenere createdBycome valore invece che come oggetto?
Jonas

setSerializationView()sembrano essere deprecati quindi ho usato mapper.viewWriter(JacksonViews.ItemView.class).writeValue(writer, myItem);invece.
Jonas

Ne dubito usando jsonviews. Una soluzione rapida e sporca che ho usato prima di scoprire le viste era semplicemente copiare le proprietà che mi interessavano, in una mappa, quindi serializzare la mappa.
Paul M

5

Nel mio caso (Spring 3.2.4 e Jackson 2.3.1), configurazione XML per serializzatore personalizzato:

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

è stato in un modo inspiegabile sovrascritto di default da qualcosa.

Questo ha funzionato per me:

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

Nessuna configurazione XML ( <mvc:message-converters>(...)</mvc:message-converters>) è necessaria nella mia soluzione.


1

Se il tuo unico requisito nel serializzatore personalizzato è saltare la serializzazione del namecampo di User, contrassegnalo come transitorio . Jackson non serializzerà o deserializzerà il transitorio campi .

[vedi anche: Perché Java ha campi transitori? ]


Dove lo contrassegno? Nella Userclasse? Ma serializzerò anche tutti gli oggetti utente. Ad esempio, prima serializza solo tutto items(con solo userIdun riferimento all'oggetto utente) e poi serializza tutto users. In questo caso non posso contrassegnare i campi nella -class User.
Jonas

Alla luce di queste nuove informazioni, questo approccio non funzionerà per te. Sembra che Jackson stia cercando ulteriori informazioni per il serializzatore personalizzato (il metodo handledType () deve essere sovrascritto?)
Mike G

Sì, ma non c'è nulla sul handledType()metodo nella documentazione a cui ho collegato e quando Eclipse genera i metodi per implementare no handledType()viene generato, quindi sono confuso.
Jonas

Non sono sicuro perché il wiki che hai collegato non fa riferimento ad esso, ma nella versione 1.5.1 c'è un handledType () e l'eccezione sembra lamentare che il metodo manca o non è valido (la classe base restituisce null dal metodo). jackson.codehaus.org/1.5.1/javadoc/org/codehaus/jackson/map/…
Mike G

1

Devi sovrascrivere il metodo handledType e tutto funzionerà

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

0

Il problema nel tuo caso è che in ItemSerializer manca il metodo handledType () che deve essere sovrascritto da 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;
   }
}

Quindi stai ottenendo l'errore esplicito che handledType () non è definito

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

Spero che aiuti qualcuno. Grazie per aver letto la mia risposta.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.