Jackson JSON custom serialization for certain fields
Asked Answered
P

8

125

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.

Pyroclastic answered 20/8, 2012 at 23:54 Comment(1)
I wrote a post about How to Write a Custom Serializer with Jackson that may be helpful to some.Shoer
C
141

You can implement a custom serializer as follows:

import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.core.JsonGenerator

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.

Carlow answered 21/8, 2012 at 0:25 Comment(11)
Jackson-databind (at least 2.1.3) already contains special ToStringSerializer, see my answer.Tessietessier
@KevinBowersox Can you help with my deserializing problem please?Hydrosome
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.Citric
Is there any less terrible way to do this? Like Person implements ToJson?Rebellious
@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).Sonnysonobuoy
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.Subsumption
@Subsumption 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.Rockyrococo
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 #54332890Claudio
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 :)Heteropterous
In kotlin, you can use reserved keywords by surrounding them with backticks. @HeteropterousWillett
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?Pome
T
72

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:
}
Tessietessier answered 14/2, 2013 at 11:24 Comment(3)
What about the reverse where a String needs to be converted to an int? I don't see ToIntSerializer.class.Pitcher
@Pitcher You might have to write a custom deserializerFumatorium
ToStringSerializer works but FloatSerializer brings this message: Could not write content: java.lang.Integer cannot be cast to java.lang.FloatAssistant
W
20

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");
    }
}
Weighty answered 5/4, 2019 at 9:34 Comment(0)
W
18

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"}
Walkup answered 15/8, 2017 at 13:51 Comment(0)
M
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"}
    }
}
Moldboard answered 21/8, 2012 at 0:13 Comment(0)
Y
2

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.

Yarber answered 12/7, 2016 at 6:25 Comment(0)
P
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");
    }
  }
}
Preciado answered 17/12, 2020 at 18:47 Comment(0)
S
0

I ended up combining the two answers (and using the implementation provided by ToStringSerializer)

POJO:

public class MyPojo{
  LocalDate date;

  @JsonSerialize(using = CurrencySerializer.class)
  double amount;
}

Serializer:

import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializerBase;

@JacksonStdImpl
public class CurrencySerializer extends ToStringSerializerBase
{
  public final static CurrencySerializer instance = new CurrencySerializer();

  public CurrencySerializer() { super(Object.class); }


  public CurrencySerializer(Class<?> handledType) {
    super(handledType);
  }

  @Override
  public final String valueToString(Object value) {
    return "$ " + value.toString();
  }
}
Salo answered 21/9, 2023 at 11:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.