121

Is there a way using Jackson JSON Processor to do custom field level serialization? For example, I'd like to have the class

public class Person {
    public String name;
    public int age;
    public int favoriteNumber;
}

serialized to the follow JSON:

{ "name": "Joe", "age": 25, "favoriteNumber": "123" }

Note that age=25 is encoded as a number while favoriteNumber=123 is encoded as a string. Out of the box Jackson marshalls int to a number. In this case I want favoriteNumber to be encoded as a string.

Steve Kuo
  • 61,876
  • 75
  • 195
  • 257
  • 1
    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 15:02

7 Answers7

134

You can implement a custom serializer as follows:

public class Person {
    public String name;
    public int age;
    @JsonSerialize(using = IntToStringSerializer.class, as=String.class)
    public int favoriteNumber:
}


public class IntToStringSerializer extends JsonSerializer<Integer> {

    @Override
    public void serialize(Integer tmpInt, 
                          JsonGenerator jsonGenerator, 
                          SerializerProvider serializerProvider) 
                          throws IOException, JsonProcessingException {
        jsonGenerator.writeObject(tmpInt.toString());
    }
}

Java should handle the autoboxing from int to Integer for you.

Kevin Bowersox
  • 93,289
  • 19
  • 159
  • 189
  • 6
    Jackson-databind (at least 2.1.3) already contains special ToStringSerializer, see my answer. – werupokz Feb 14 '13 at 11:26
  • @KevinBowersox Can you help with my [deserializing problem](http://stackoverflow.com/questions/26725278/how-to-handle-localized-decimal-separator-with-jackson) please? – JJD Nov 04 '14 at 23:46
  • 9
    note `as=String.class` is ignored due to the `using` parameter, and is not required here. [*Note: if using() is also used it has precedence (since it directly specified serializer, whereas this would only be used to locate the serializer) and value of this annotation property is ignored.*](http://jackson.codehaus.org/1.7.0/javadoc/org/codehaus/jackson/map/annotate/JsonSerialize.html#as()) – Gareth Latty Apr 15 '15 at 14:36
  • Is there any less terrible way to do this? Like `Person implements ToJson`? – jameshfisher Oct 01 '15 at 11:16
  • @KevinBowersox There is probably a typo in the first words of the answer: you meant "serializer", not "deserializer" (though deserializer may also be of insterest). – Mikhail Batcer Feb 11 '16 at 13:22
  • 1
    In my case, it even failed on the `as=String.class` part, due to the types I used. @kevin-bowersox, I suggest updating your comment, in line with what @GarethLatty said. – Bert Oct 17 '17 at 19:21
  • @Bert I second that. I get a `Cannot read latest test results com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Invalid definition for property "instant" (of type `com.MyType`): Cannot refine serialization type [simple type, class java.time.Instant] into java.lang.String; types not related` message, when I don't remove the `as`-annotation. – Torsten Feb 14 '18 at 15:27
  • we were using custom serialization but now i can't use POJOs anymore as more clients will be accessing our rest service. how can i still custom serialize certain fields ? just posted this question https://stackoverflow.com/questions/54332390/custom-serialization-based-on-attribute-in-json – Sammy Pawar Jan 23 '19 at 17:26
  • In Kotlin, it's impossible to write "as = " since "as" is a reserved keyword. At least not without any hacks I haven't bothered to look up yet :) – kit Jun 04 '20 at 16:10
  • 1
    In kotlin, you can use reserved keywords by surrounding them with backticks. @kit – Cebrail Yilmaz May 03 '21 at 14:43
  • This serializes for types and not fields. How do I serialize specific `fields`? Does this solution not also map `age` to a string or does it maintain it as an `int`? – orpheus Sep 07 '21 at 18:18
68

Jackson-databind (at least 2.1.3) provides special ToStringSerializer (com.fasterxml.jackson.databind.ser.std.ToStringSerializer)

Example:

public class Person {
    public String name;
    public int age;
    @JsonSerialize(using = ToStringSerializer.class)
    public int favoriteNumber:
}
werupokz
  • 746
  • 6
  • 4
19

In case you don't want to pollute your model with annotations and want to perform some custom operations, you could use mixins.

ObjectMapper mapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.setMixInAnnotation(Person.class, PersonMixin.class);
mapper.registerModule(simpleModule);

Override age:

public abstract class PersonMixin {
    @JsonSerialize(using = PersonAgeSerializer.class)
    public String age;
}

Do whatever you need with the age:

public class PersonAgeSerializer extends JsonSerializer<Integer> {
    @Override
    public void serialize(Integer integer, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeString(String.valueOf(integer * 52) + " months");
    }
}
Igor G.
  • 6,955
  • 6
  • 26
  • 26
17

jackson-annotations provides @JsonFormat which can handle a lot of customizations without the need to write the custom serializer.

For example, requesting a STRING shape for a field with numeric type will output the numeric value as string

public class Person {
    public String name;
    public int age;
    @JsonFormat(shape = JsonFormat.Shape.STRING)
    public int favoriteNumber;
}

will result in the desired output

{"name":"Joe","age":25,"favoriteNumber":"123"}
Oleg Estekhin
  • 8,063
  • 5
  • 49
  • 52
16

Add a @JsonProperty annotated getter, which returns a String, for the favoriteNumber field:

public class Person {
    public String name;
    public int age;
    private int favoriteNumber;

    public Person(String name, int age, int favoriteNumber) {
        this.name = name;
        this.age = age;
        this.favoriteNumber = favoriteNumber;
    }

    @JsonProperty
    public String getFavoriteNumber() {
        return String.valueOf(favoriteNumber);
    }

    public static void main(String... args) throws Exception {
        Person p = new Person("Joe", 25, 123);
        ObjectMapper mapper = new ObjectMapper();
        System.out.println(mapper.writeValueAsString(p)); 
        // {"name":"Joe","age":25,"favoriteNumber":"123"}
    }
}
João Silva
  • 89,303
  • 29
  • 152
  • 158
3

with the help of @JsonView we can decide fields of model classes to serialize which satisfy the minimal criteria ( we have to define the criteria) like we can have one core class with 10 properties but only 5 properties can be serialize which are needful for client only

Define our Views by simply creating following class:

public class Views
{
    static class Android{};
    static class IOS{};
    static class Web{};
}

Annotated model class with views:

public class Demo 
{
    public Demo() 
    {
    }

@JsonView(Views.IOS.class)
private String iosField;

@JsonView(Views.Android.class)
private String androidField;

@JsonView(Views.Web.class)
private String webField;

 // getters/setters
...
..
}

Now we have to write custom json converter by simply extending HttpMessageConverter class from spring as:

    public class CustomJacksonConverter implements HttpMessageConverter<Object> 
    {
    public CustomJacksonConverter() 
        {
            super();
        //this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView(Views.ClientView.class));
        this.delegate.getObjectMapper().configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
        this.delegate.getObjectMapper().setSerializationInclusion(Include.NON_NULL);

    }

    // a real message converter that will respond to methods and do the actual work
    private MappingJackson2HttpMessageConverter delegate = new MappingJackson2HttpMessageConverter();

    @Override
    public boolean canRead(Class<?> clazz, MediaType mediaType) {
        return delegate.canRead(clazz, mediaType);
    }

    @Override
    public boolean canWrite(Class<?> clazz, MediaType mediaType) {
        return delegate.canWrite(clazz, mediaType);
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return delegate.getSupportedMediaTypes();
    }

    @Override
    public Object read(Class<? extends Object> clazz,
            HttpInputMessage inputMessage) throws IOException,
            HttpMessageNotReadableException {
        return delegate.read(clazz, inputMessage);
    }

    @Override
    public void write(Object obj, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException 
    {
        synchronized(this) 
        {
            String userAgent = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getHeader("userAgent");
            if ( userAgent != null ) 
            {
                switch (userAgent) 
                {
                case "IOS" :
                    this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView(Views.IOS.class));
                    break;
                case "Android" :
                    this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView(Views.Android.class));
                    break;
                case "Web" :
                    this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView( Views.Web.class));
                    break;
                default:
                    this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView( null ));
                    break;
                }
            }
            else
            {
                // reset to default view
                this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView( null ));
            }
            delegate.write(obj, contentType, outputMessage);
        }
    }

}

Now there is need to tell spring to use this custom json convert by simply putting this in dispatcher-servlet.xml

<mvc:annotation-driven>
        <mvc:message-converters register-defaults="true">
            <bean id="jsonConverter" class="com.mactores.org.CustomJacksonConverter" >
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

That's how you will able to decide which fields to get serialize.

1

You can create a custom serializer inline in the mixin. Then annotate a field with it. See example below that appends " - something else " to lang field. This is kind of hackish - if your serializer requires something like a repository or anything injected by spring, this is going to be a problem. Probably best to use a custom deserializer/serializer instead of a mixin.

package com.test;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.test.Argument;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
//Serialize only fields explicitly mentioned by this mixin.
@JsonAutoDetect(
    fieldVisibility = Visibility.NONE,
    setterVisibility = Visibility.NONE,
    getterVisibility = Visibility.NONE,
    isGetterVisibility = Visibility.NONE,
    creatorVisibility = Visibility.NONE
)
@JsonPropertyOrder({"lang", "name", "value"})
public abstract class V2ArgumentMixin {

  @JsonProperty("name")
  private String name;

  @JsonSerialize(using = LangCustomSerializer.class, as=String.class)
  @JsonProperty("lang")
  private String lang;

  @JsonProperty("value")
  private Object value;


  
  public static class LangCustomSerializer extends JsonSerializer<String> {

    @Override
    public void serialize(String value,
                          JsonGenerator jsonGenerator,
                          SerializerProvider serializerProvider)
        throws IOException, JsonProcessingException {
      jsonGenerator.writeObject(value.toString() + "  - something else");
    }
  }
}
Vladimir
  • 1,120
  • 2
  • 11
  • 18