127

I have two Java classes that I want to serialize to JSON using 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;
    }
}

I want to serialize an Item to this JSON:

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

with User serialized to only include the id. I will also be able to serilize all user objects to JSON like:

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

So I guess that I need to write a custom serializer for Item and tried with this:

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

}

I serialize the JSON with this code from 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();
}

But I get this 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)

How can I use a custom Serializer with Jackson?


This is how I would do it with 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);

But I need to do it with Jackson now, since Gson doesn't have support for interfaces.

Konstantin
  • 3,626
  • 2
  • 33
  • 45
Jonas
  • 121,568
  • 97
  • 310
  • 388
  • how / where did you get Jackson to use your custom Serializer for the `Item`? I'm having an issue where my controller method returns a standard serialized object `TypeA`, but for another specific controller method, I want to serialize it differently. What would that look like? – Don Cheadle Sep 29 '15 at 19:26
  • I wrote a post about [How to Write a Custom Serializer with Jackson](https://spin.atomicobject.com/2016/07/01/custom-serializer-jackson/?utm_source=stack-overflow-ao&utm_medium=referral&utm_campaign=custom-serializer-jackson) that may be helpful to some. – Sam Berry Jul 01 '16 at 14:57

11 Answers11

69

You can put @JsonSerialize(using = CustomDateSerializer.class) over any date field of object to be serialized.

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

}
Daniel
  • 8,655
  • 5
  • 60
  • 87
Moesio
  • 3,100
  • 1
  • 27
  • 36
  • 1
    worth noting: use `@JsonSerialize(contentUsing= ...)` when annotating Collections (e.g. `@JsonSerialize(contentUsing= CustomDateSerializer.class) List dates`) – coderatchet Jul 17 '17 at 03:17
53

As mentioned, @JsonValue is a good way. But if you don't mind a custom serializer, there's no need to write one for Item but rather one for User -- if so, it'd be as simple as:

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

Yet another possibility is to implement JsonSerializable, in which case no registration is needed.

As to error; that is weird -- you probably want to upgrade to a later version. But it is also safer to extend org.codehaus.jackson.map.ser.SerializerBase as it will have standard implementations of non-essential methods (i.e. everything but actual serialization call).

StaxMan
  • 113,358
  • 34
  • 211
  • 239
  • With this I get the same 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.(JsonTest.java:27) at com.exampple.JsonTest.main(JsonTest.java:102)` – Jonas Aug 24 '11 at 09:14
  • I use the latest stable version of Jacskson, 1.8.5. – Jonas Aug 24 '11 at 09:15
  • 4
    Thanks. I'll have a look... Ah! It's actually simple (although error message is not good) -- you just need to register serializer with different method, to specify class that serializer is for: if not, it must return class from handledType(). So use 'addSerializer' that takes JavaType or Class as argument and it should work. – StaxMan Aug 24 '11 at 17:25
  • What if this is not being run? – Matej J Mar 16 '20 at 10:59
46

I tried doing this too, and there is a mistake in the example code on the Jackson web page that fails to include the type (.class) in the call to addSerializer() method, which should read like this:

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

In other words, these are the lines that instantiate the simpleModule and add the serializer (with the prior incorrect line commented out):

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: Here is the reference for the correct example code: http://wiki.fasterxml.com/JacksonFeatureModules

Ihor Patsian
  • 1,288
  • 2
  • 15
  • 25
pmhargis
  • 713
  • 6
  • 7
12

Use @JsonValue:

public class User {
    int id;
    String name;

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

@JsonValue only works on methods so you must add the getId method. You should be able to skip your custom serializer altogether.

Henrik Barratt Due
  • 5,614
  • 1
  • 21
  • 22
  • 2
    I think this will impact all attempts to serialise a User, making it difficult to ever expose a User's name over JSON. – Paul M Aug 24 '11 at 08:46
  • I can't use this solution, because I also need to be able to serialize all user objects with all fields. And this solution will break that serialization since only the id-field will be included. Is there no way to create a custom serilizer for Jackson as it is for Gson? – Jonas Aug 24 '11 at 08:57
  • 1
    Can you comment on why JSON Views (in my answer) don't match your needs? – Paul M Aug 24 '11 at 09:47
  • @user: It may be a good solution, I'm reading about it and trying. – Jonas Aug 24 '11 at 10:38
  • 2
    Note, too, that you can use @JsonSerialize(using=MySerializer.class) to indicate specific serialization for your property (field or getter), so it is only used for member property and NOT all instances of type. – StaxMan Aug 26 '11 at 04:54
10

I wrote an example for a custom Timestamp.class serialization/deserialization, but you could use it for what ever you want.

When creating the object mapper do something like this:

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

for example in java ee you could initialize it with this:

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

where the serializer should be something like this:

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

and deserializer something like this:

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
  • 6,723
  • 4
  • 55
  • 59
9

These are behavior patterns I have noticed while trying to understand Jackson serialization.

1) Assume there is an object Classroom and a class Student. I've made everything public and final for ease.

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) Assume that these are the serializers we use for serializing the objects into JSON. The writeObjectField uses the object's own serializer if it is registered with the object mapper; if not, then it serializes it as a POJO. The writeNumberField exclusively only accepts primitives as 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) Register only a DoubleSerializer with DecimalFormat output pattern ###,##0.000, in SimpleModule and the output is:

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

You can see that the POJO serialization differentiates between double and Double, using the DoubleSerialzer for Doubles and using a regular String format for doubles.

4) Register DoubleSerializer and ClassroomSerializer, without the StudentSerializer. We expect that the output is such that if we write a double as an object, it behaves like a Double, and if we write a Double as a number, it behaves like a double. The Student instance variable should be written as a POJO and follow the pattern above since it does not register.

{
  "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) Register all serializers. The output is:

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

exactly as expected.

Another important note: If you have multiple serializers for the same class registered with the same Module, then the Module will select the serializer for that class that is most recently added to the list. This should not be used - it's confusing and I am not sure how consistent this is

Moral: if you want to customize serialization of primitives in your object, you must write your own serializer for the object. You cannot rely on the POJO Jackson serialization.

laughing_man
  • 3,756
  • 1
  • 20
  • 20
6

Jackson's JSON Views might be a simpler way of achieving your requirements, especially if you have some flexibility in your JSON format.

If {"id":7, "itemNr":"TEST", "createdBy":{id:3}} is an acceptable representation then this will be very easy to achieve with very little code.

You would just annotate the name field of User as being part of a view, and specify a different view in your serialisation request (the un-annotated fields would be included by default)

For example: Define the views:

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

Annotate the User:

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

And serialise requesting a view which doesn't contain the field you want to hide (non-annotated fields are serialised by default):

objectMapper.getSerializationConfig().withView(Views.BasicView.class);
Ben
  • 60,438
  • 111
  • 314
  • 488
Paul M
  • 357
  • 1
  • 8
  • I find the Jackson JSON Views hard to use, and can't get a good solution for this problem. – Jonas Aug 24 '11 at 11:59
  • Jonas - I've added an example. I found views a really nice solution for serialising the same object in different ways. – Paul M Aug 24 '11 at 12:24
  • Thanks for a good example. This is the best solution so far. But is there no way to get `createdBy` as a value instead of as an object? – Jonas Aug 24 '11 at 12:53
  • `setSerializationView()` seem to be deprecated so I used `mapper.viewWriter(JacksonViews.ItemView.class).writeValue(writer, myItem);` instead. – Jonas Aug 24 '11 at 14:23
  • I doubt it using jsonviews. A quick & dirty solution that I used before discovering views was just to copy the properties I was interested in, into a Map, and then serialise the map. – Paul M Aug 24 '11 at 15:46
  • Use @JsonSerialize(using=IdSerializer.class) for 'createdBy' field, to produce different serialization from default. – StaxMan Aug 26 '11 at 04:54
  • The `mapper.viewWriter` method is deprecated since 1.9 and is now `mapper.writerWithView(JacksonViews.ItemView.class).writeValue(writer, myItem);` – Phil McCullick Feb 02 '12 at 21:54
5

In my case (Spring 3.2.4 and Jackson 2.3.1), XML configuration for custom serializer:

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

was in unexplained way overwritten back to default by something.

This worked for 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;
    }
}

No XML configuration (<mvc:message-converters>(...)</mvc:message-converters>) is needed in my solution.

user11153
  • 8,536
  • 5
  • 47
  • 50
2

The problem in your case is the ItemSerializer is missing the method handledType() which needs to be overridden from 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;
   }
}

Hence you are getting the explicit error that handledType() is not defined

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

Hope it helps someone. Thanks for reading my answer.

Sathiamoorthy
  • 8,831
  • 9
  • 65
  • 77
Sanjay Bharwani
  • 3,317
  • 34
  • 31
1

You have to override method handledType and everything will work

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

If your only requirement in your custom serializer is to skip serializing the name field of User, mark it as transient. Jackson will not serialize or deserialize transient fields.

[ see also: Why does Java have transient fields? ]

Community
  • 1
  • 1
Mike G
  • 4,713
  • 2
  • 28
  • 31
  • Where do I mark it? In the `User`-class? But I will serialize all user-objects too. E.g. first only serialize all `items` (with only `userId` as a reference to the user object) and then serialize all `users`. In this case I can't mark the fiels in the `User`-class. – Jonas Aug 23 '11 at 13:29
  • In light of this new information, this approach won't work for you. It looks like Jackson is looking for more information for the custom serializer (handledType() method needs overriding?) – Mike G Aug 23 '11 at 13:32
  • Yes, but there is nothging about the `handledType()` method in the documentation I linked to and when Eclipse generates the methods to implement no `handledType()` is generated, so I'm confused. – Jonas Aug 23 '11 at 13:36
  • I'm not sure because the wiki you linked doesn't reference it, but in version 1.5.1 there is a handledType() and the exception seems to be complaining that method missing or invalid (base class returns null from the method). http://jackson.codehaus.org/1.5.1/javadoc/org/codehaus/jackson/map/JsonSerializer.html#handledType() – Mike G Aug 23 '11 at 13:42