Comment utiliser un sérialiseur personnalisé avec Jackson?

111

J'ai deux classes Java que je souhaite sérialiser en JSON à l'aide de 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;
    }
}

Je souhaite sérialiser un élément dans ce JSON:

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

avec l'utilisateur sérialisé pour inclure uniquement le fichier id. Je pourrai également sériliser tous les objets utilisateur en JSON comme:

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

Je suppose donc que j'ai besoin d'écrire un sérialiseur personnalisé Itemet d'essayer ceci:

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

}

Je sérialise le JSON avec ce code 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();
}

Mais j'obtiens cette erreur:

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)

Comment puis-je utiliser un sérialiseur personnalisé avec Jackson?


Voici comment je le ferais avec 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);

Mais je dois le faire avec Jackson maintenant, puisque Gson ne prend pas en charge les interfaces.

Jonas
la source
comment / où avez-vous amené Jackson à utiliser votre sérialiseur personnalisé pour le Item? J'ai un problème où ma méthode de contrôleur renvoie un objet sérialisé standard TypeA, mais pour une autre méthode de contrôleur spécifique, je veux le sérialiser différemment. À quoi cela ressemblerait-il?
Don Cheadle
J'ai écrit un article sur Comment écrire un sérialiseur personnalisé avec Jackson qui peut être utile à certains.
Sam Berry

Réponses:

51

Comme mentionné, @JsonValue est un bon moyen. Mais si un sérialiseur personnalisé ne vous dérange pas, il n'est pas nécessaire d'en écrire un pour Item mais plutôt un pour User - si c'est le cas, ce serait aussi simple que:

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

Une autre possibilité est de mettre en œuvre JsonSerializable, auquel cas aucun enregistrement n'est nécessaire.

Quant à l'erreur; c'est bizarre - vous voulez probablement passer à une version ultérieure. Mais il est également plus sûr de l'étendre org.codehaus.jackson.map.ser.SerializerBasecar il aura des implémentations standard de méthodes non essentielles (c'est-à-dire tout sauf un appel de sérialisation réel).

StaxMan
la source
Avec cela, j'obtiens la même erreur: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
J'utilise la dernière version stable de Jacskson, 1.8.5.
Jonas
4
Merci. Je vais jeter un oeil ... Ah! C'est en fait simple (bien que le message d'erreur ne soit pas bon) - il vous suffit d'enregistrer le sérialiseur avec une méthode différente, pour spécifier la classe à laquelle le sérialiseur est destiné: sinon, il doit renvoyer la classe de handleType (). Utilisez donc 'addSerializer' qui prend JavaType ou Class comme argument et cela devrait fonctionner.
StaxMan
Et si cela n'est pas exécuté?
Matej J
62

Vous pouvez placer @JsonSerialize(using = CustomDateSerializer.class)n'importe quel champ de date de l'objet à sérialiser.

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
la source
1
à noter: à utiliser @JsonSerialize(contentUsing= ...)lors de l'annotation des collections (par exemple @JsonSerialize(contentUsing= CustomDateSerializer.class) List<Date> dates)
coderatchet
33

J'ai essayé de faire cela aussi, et il y a une erreur dans l'exemple de code sur la page Web de Jackson qui ne parvient pas à inclure le type ( .class) dans l'appel à la addSerializer()méthode, qui devrait se lire comme ceci:

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

En d'autres termes, ce sont les lignes qui instancient le simpleModuleet ajoutent le sérialiseur (avec la ligne incorrecte précédente commentée):

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: Voici la référence pour le code d'exemple correct: http://wiki.fasterxml.com/JacksonFeatureModules

pmhargis
la source
9

Utilisez @JsonValue:

public class User {
    int id;
    String name;

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

@JsonValue ne fonctionne que sur les méthodes, vous devez donc ajouter la méthode getId. Vous devriez pouvoir ignorer complètement votre sérialiseur personnalisé.

henrik_lundgren
la source
2
Je pense que cela aura un impact sur toutes les tentatives de sérialisation d'un utilisateur, ce qui rendra difficile d'exposer le nom d'un utilisateur via JSON.
Paul M
Je ne peux pas utiliser cette solution, car j'ai également besoin de pouvoir sérialiser tous les objets utilisateur avec tous les champs. Et cette solution interrompra cette sérialisation puisque seul le champ id sera inclus. N'existe-t-il aucun moyen de créer un sérilisateur personnalisé pour Jackson comme pour Gson?
Jonas
1
Pouvez-vous expliquer pourquoi les vues JSON (dans ma réponse) ne correspondent pas à vos besoins?
Paul M
@user: C'est peut-être une bonne solution, je lis et j'essaye.
Jonas
2
Notez également que vous pouvez utiliser @JsonSerialize (en utilisant = MySerializer.class) pour indiquer une sérialisation spécifique pour votre propriété (champ ou getter), elle n'est donc utilisée que pour la propriété membre et PAS toutes les instances de type.
StaxMan
8

J'ai écrit un exemple pour une Timestamp.classsérialisation / désérialisation personnalisée, mais vous pouvez l'utiliser pour ce que vous voulez.

Lors de la création du mappeur d'objets, procédez comme suit:

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

par exemple, java eevous pouvez l'initialiser avec ceci:

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

où le sérialiseur devrait être quelque chose comme ceci:

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

et désérialiseur quelque chose comme ceci:

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

Ce sont des modèles de comportement que j'ai remarqués en essayant de comprendre la sérialisation de Jackson.

1) Supposons qu'il y ait un objet Classroom et un class Student. J'ai tout rendu public et définitif pour plus de 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) Supposons que ce sont les sérialiseurs que nous utilisons pour sérialiser les objets dans JSON. WriteObjectField utilise le propre sérialiseur de l'objet s'il est enregistré avec le mappeur d'objet; sinon, il le sérialise en tant que POJO. Le writeNumberField n'accepte exclusivement que les primitives comme arguments.

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) Enregistrez uniquement un DoubleSerializer avec un modèle de sortie DecimalFormat ###,##0.000, dans SimpleModule et la sortie est:

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

Vous pouvez voir que la sérialisation POJO fait la différence entre double et Double, en utilisant le DoubleSerialzer pour les doubles et en utilisant un format de chaîne standard pour les doubles.

4) Enregistrez DoubleSerializer et ClassroomSerializer, sans le StudentSerializer. Nous nous attendons à ce que la sortie soit telle que si nous écrivons un double comme un objet, il se comporte comme un Double, et si nous écrivons un Double comme un nombre, il se comporte comme un double. La variable d'instance Student doit être écrite en tant que POJO et suivre le modèle ci-dessus car elle ne s'enregistre pas.

{
  "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) Enregistrez tous les sérialiseurs. La sortie est:

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

exactement comme prévu.

Une autre note importante: si vous avez plusieurs sérialiseurs pour la même classe enregistrés avec le même module, le module sélectionnera le sérialiseur pour cette classe qui est le plus récemment ajouté à la liste. Cela ne devrait pas être utilisé - c'est déroutant et je ne sais pas à quel point c'est cohérent

Morale: si vous souhaitez personnaliser la sérialisation des primitives dans votre objet, vous devez écrire votre propre sérialiseur pour l'objet. Vous ne pouvez pas vous fier à la sérialisation de POJO Jackson.

rire_homme
la source
Comment enregistrer ClassroomSerializer pour gérer par exemple les occurrences de Classroom?
Trismegistos
5

Les vues JSON de Jackson peuvent être un moyen plus simple de répondre à vos besoins, en particulier si vous disposez d'une certaine flexibilité dans votre format JSON.

Si {"id":7, "itemNr":"TEST", "createdBy":{id:3}}c'est une représentation acceptable, alors cela sera très facile à réaliser avec très peu de code.

Vous devez simplement annoter le champ de nom de l'utilisateur comme faisant partie d'une vue et spécifier une vue différente dans votre demande de sérialisation (les champs non annotés seraient inclus par défaut)

Par exemple: définissez les vues:

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

Annotez l'utilisateur:

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

Et sérialiser en demandant une vue qui ne contient pas le champ que vous souhaitez masquer (les champs non annotés sont sérialisés par défaut):

objectMapper.getSerializationConfig().withView(Views.BasicView.class);
Paul M
la source
Je trouve que les vues Jackson JSON sont difficiles à utiliser et je ne parviens pas à trouver une bonne solution à ce problème.
Jonas
Jonas - J'ai ajouté un exemple. J'ai trouvé que les vues étaient une solution vraiment intéressante pour sérialiser le même objet de différentes manières.
Paul M
Merci pour un bon exemple. C'est la meilleure solution à ce jour. Mais n'y a-t-il aucun moyen d'obtenir createdBycomme valeur plutôt que comme objet?
Jonas
setSerializationView()semblent être obsolètes, alors j'ai utilisé à la mapper.viewWriter(JacksonViews.ItemView.class).writeValue(writer, myItem);place.
Jonas
J'en doute en utilisant jsonviews. Une solution rapide et sale que j'ai utilisée avant de découvrir des vues consistait simplement à copier les propriétés qui m'intéressaient, dans une carte, puis à sérialiser la carte.
Paul M
5

Dans mon cas (Spring 3.2.4 et Jackson 2.3.1), configuration XML pour le sérialiseur personnalisé:

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

a été de manière inexpliquée réécrit par défaut par quelque chose.

Cela a fonctionné pour moi:

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

Aucune configuration XML ( <mvc:message-converters>(...)</mvc:message-converters>) n'est nécessaire dans ma solution.

user11153
la source
1

Si votre seule exigence dans votre sérialiseur personnalisé est d'ignorer la sérialisation du namechamp de User, marquez-le comme transitoire . Jackson ne sérialisera ni ne désérialisera les champs transitoires .

[voir aussi: Pourquoi Java a-t-il des champs transitoires? ]

Mike G
la source
Où dois-je le marquer? Dans la Userclasse? Mais je sérialiserai aussi tous les objets utilisateur. Par exemple, commencez par sérialiser tout items(avec uniquement userIdcomme référence à l'objet utilisateur), puis sérialisez tout users. Dans ce cas, je ne peux pas marquer les champs dans la User-classe.
Jonas
À la lumière de ces nouvelles informations, cette approche ne fonctionnera pas pour vous. Il semble que Jackson cherche plus d'informations sur le sérialiseur personnalisé (la méthode handleType () doit être remplacée?)
Mike G
Oui, mais il n'y a rien sur la handledType()méthode dans la documentation à laquelle j'ai lié et quand Eclipse génère les méthodes pour implémenter no handledType()est généré, donc je suis confus.
Jonas
Je ne suis pas sûr car le wiki que vous avez lié ne le fait pas référence, mais dans la version 1.5.1, il y a un handleType () et l'exception semble se plaindre de cette méthode manquante ou invalide (la classe de base renvoie null de la méthode). jackson.codehaus.org/1.5.1/javadoc/org/codehaus/jackson/map/…
Mike G
1

Vous devez remplacer la méthode handleType et tout fonctionnera

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

Le problème dans votre cas est qu'il manque à ItemSerializer la méthode handleType () qui doit être remplacée par 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;
   }
}

Par conséquent, vous obtenez l'erreur explicite que handleType () n'est pas défini

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

J'espère que ça aide quelqu'un. Merci d'avoir lu ma réponse.

Sanjay Bharwani
la source