How do I use a custom Serializer with Jackson?
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).
Swap Jackson custom serializer / deserializer during runtime
This is my solution
It's not pretty but does its job.
I left my old jackson config untouched, so the client<->server serialization stays the same.
I then added this custom ObjectMapper to take care of my server<->file.
My custom ObjectMapper does the following things:
- It registers a new custom JacksonAnnotationIntrospector, which I configured to ignore certain annotations. I also configured it to use my selfmade annotation
@TransferJsonTypeInfo
whenever a property has both the@TransferJsonTypeInfo
as well as the@JsonTypeInfo
annotation. - I registered my
CustomerFileSerializer
andCustomerFileDeserializer
for this ObjectMapper.
@Service
public class ImportExportMapper {
protected final ObjectMapper customObjectMapper;
private static final JacksonAnnotationIntrospector IGNORE_JSON_ANNOTATIONS_AND_USE_TRANSFERJSONTYPEINFO = BuildImportExportJacksonAnnotationIntrospector();
public ImportExportMapper(){
customObjectMapper = new ObjectMapper().registerModule(new JavaTimeModule())
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);
// emulate the default settings as described here: https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-customize-the-jackson-objectmapper
customObjectMapper.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
customObjectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule module = new SimpleModule();
module.addSerializer(Customer.class, new CustomerFileSerializer());
module.addDeserializer(Customer.class, new CustomerFileDeserializer());
customObjectMapper.setAnnotationIntrospector(IGNORE_JSON_ANNOTATIONS_AND_USE_TRANSFERJSONTYPEINFO);
customObjectMapper.registerModule(module);
}
public String writeValueAsString(Object data) {
try {
return customObjectMapper.writeValueAsString(data);
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new IllegalArgumentException();
}
}
public ObjectTransferData readValue(String fileContent, Class clazz) throws JsonProcessingException {
return customObjectMapper.readValue(fileContent, clazz);
}
private static JacksonAnnotationIntrospector BuildImportExportJacksonAnnotationIntrospector() {
return new JacksonAnnotationIntrospector() {
@Override
protected <A extends Annotation> A _findAnnotation(final Annotated annotated, final Class<A> annoClass) {
if (annoClass == JsonTypeInfo.class && _hasAnnotation(annotated, FileJsonTypeInfo.class)) {
FileJsonTypeInfo fileJsonTypeInfo = _findAnnotation(annotated, TransferJsonTypeInfo.class);
if(fileJsonTypeInfo != null && fileJsonTypeInfo.jsonTypeInfo() != null) {
return (A) fileJsonTypeInfo.jsonTypeInfo(); // this cast should be safe because we have checked the annotation class
}
}
if (ignoreJsonAnnotations(annoClass)) return null;
return super._findAnnotation(annotated, annoClass);
}
};
}
private static <A extends Annotation> boolean ignoreJsonAnnotations(Class<A> annoClass) {
if (annoClass == JsonSerialize.class) {
return true;
}
if(annoClass == JsonDeserialize.class){
return true;
}
if(annoClass == JsonIdentityReference.class){
return true;
}
return annoClass == JsonIdentityInfo.class;
}
}
My custom annotation is defined and described like this:
/**
* This annotation inside of a annotation solution is a way to tell the importExportMapper how to serialize/deserialize
* objects that already have a wrongly defined @JsonTypeInfo annotation (wrongly defined for the importExportMapper).
*
* Idea is taken from here: https://stackoverflow.com/questions/58495480/how-to-properly-override-jacksonannotationintrospector-findannotation-to-replac
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FileJsonTypeInfo {
JsonTypeInfo jsonTypeInfo();
}
And it is used like this:
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
@JsonTypeInfo(defaultImpl = Customer.class, property = "", use = JsonTypeInfo.Id.NONE)
@TransferJsonTypeInfo(jsonTypeInfo = @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "customeridentifier"))
@JsonIdentityReference(alwaysAsId = true)
@JsonDeserialize(using = CustomerClientDeserializer.class)
@JsonSerialize(using = CustomerClientSerializer.class)
private Customer customer;
Jackson custom serialization and deserialization
Idea with Unit
annotation is really good. We need to only add custom com.fasterxml.jackson.databind.ser.BeanSerializerModifier
and com.fasterxml.jackson.databind.ser.BeanPropertyWriter
implementations. Let's create first our annotation class:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Unit {
String value();
}
POJO
model could look like below:
class Pojo {
private User user = new User();
private Food food = new Food();
private House house = new House();
// getters, setters, toString
}
class User {
@Unit("m")
private int height = 10;
// getters, setters, toString
}
class Food {
@Unit("C")
private int temperature = 50;
// getters, setters, toString
}
class House {
@Unit("m")
private int width = 10;
// getters, setters, toString
}
Having all of that we need to customise property serialisation:
class UnitBeanSerializerModifier extends BeanSerializerModifier {
@Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
for (int i = 0; i < beanProperties.size(); ++i) {
final BeanPropertyWriter writer = beanProperties.get(i);
AnnotatedMember member = writer.getMember();
Unit units = member.getAnnotation(Unit.class);
if (units != null) {
beanProperties.set(i, new UnitBeanPropertyWriter(writer, units.value()));
}
}
return beanProperties;
}
}
class UnitBeanPropertyWriter extends BeanPropertyWriter {
private final String unit;
protected UnitBeanPropertyWriter(BeanPropertyWriter base, String unit) {
super(base);
this.unit = unit;
}
@Override
public void serializeAsField(Object bean, JsonGenerator gen, SerializerProvider prov) throws Exception {
gen.writeFieldName(_name);
final Object value = (_accessorMethod == null) ? _field.get(bean) : _accessorMethod.invoke(bean, (Object[]) null);
gen.writeString(value + " " + unit);
}
}
Using SimpleModule
we can register it and use with ObjectMapper
:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
public class JsonApp {
public static void main(String[] args) throws Exception {
SimpleModule unitModule = new SimpleModule();
unitModule.setSerializerModifier(new UnitBeanSerializerModifier());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(unitModule);
Pojo pojo = new Pojo();
System.out.println(mapper.writeValueAsString(pojo));
}
}
prints:
{
"user" : {
"height" : "10 m"
},
"food" : {
"temperature" : "50 C"
},
"house" : {
"width" : "10 m"
}
}
Of course, you need to test it and handle all corner cases but above example shows general idea. In the similar way we can handle deserialisation. We need to implement custom BeanDeserializerModifier
and one custom UnitDeserialiser
:
class UnitBeanDeserializerModifier extends BeanDeserializerModifier {
@Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
JsonDeserializer<?> jsonDeserializer = super.modifyDeserializer(config, beanDesc, deserializer);
if (jsonDeserializer instanceof StdScalarDeserializer) {
StdScalarDeserializer scalarDeserializer = (StdScalarDeserializer) jsonDeserializer;
Class scalarClass = scalarDeserializer.handledType();
if (int.class == scalarClass) {
return new UnitIntStdScalarDeserializer(scalarDeserializer);
}
}
return jsonDeserializer;
}
}
and example deserialiser for int
:
class UnitIntStdScalarDeserializer extends StdScalarDeserializer<Integer> {
private StdScalarDeserializer<Integer> src;
public UnitIntStdScalarDeserializer(StdScalarDeserializer<Integer> src) {
super(src);
this.src = src;
}
@Override
public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String value = p.getValueAsString();
String[] parts = value.split("\\s+");
if (parts.length == 2) {
return Integer.valueOf(parts[0]);
}
return src.deserialize(p, ctxt);
}
}
Above implementation is just an example and should be improved for other primitive types. We can register it in the same way using simple module. Reuse the same as for serialisation:
unitModule.setDeserializerModifier(new UnitBeanDeserializerModifier());
Custom Serializer and Jackson Schema
It works after I overwrite acceptJsonFormatVisitor()
in my MapValueSerializer
class with:
@Override
public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException
{
JavaType valueType = ((MapType) typeHint).getContentType();
visitor.getProvider().findValueSerializer(valueType).acceptJsonFormatVisitor(visitor, valueType);
}
Then child is of type "object" and also the subelement "x" is generated in the schema.
Global jackson ObjectMapper not registering custom serializer
You have annotated your application with @EnableWebFlux
. This indicates that you want to take complete control of WebFlux's configuration. This causes Spring Boot's auto-configuration of WebFlux to back off. Among other things, this means that it won't configure WebFlux to use the context's ObjectMapper
.
You should either remove @EnableWebFlux
to allow Spring Boot to auto-configure WebFlux or you should configure its codecs manually so that they use your ObjectMapper
.
Jackson Custom Serializer shows the same context for 2 different field during the Json Serialization
A. Create two Map
serialisers where one creates outer object and another not
Pros:
- Easy to implement
- Easy to test
- One class does exactly one thing
Map
serialiser which does not create outer object could be replaced by customMap
serialiser (if possible)
Cons:
- Could be problematic if they need to share state.
- Possibly duplicated code
B. Implement ContextualSerializer interface
Pros:
- Can be configured for every field separately
- Can share state if needed. User control how many instances are created.
Cons:
- Does more than 1 thing
- Can be easily over complicated
Examples:
- Need Jackson serializer for Double and need to specify precision at runtime
- Jackson custom annotation for custom value serialization
- Deserialize to String or Object using Jackson
- Jackson - deserialize inner list of objects to list of one higher level
Jackson won't serialize null with a custom Serializer
I may be misunderstanding what you want, but this approach seems useful:
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.HashMap;
import java.util.Map;
public class Test2 {
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface JsonAuth {
}
@JsonFilter("myFilter")
public static class Contact {
@JsonAuth
String first;
@JsonAuth
String middle;
@JsonAuth
String last;
String email;
public Contact(String first, String middle, String last, String email) {
this.first = first;
this.middle = middle;
this.last = last;
this.email = email;
}
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
public String getMiddle() {
return middle;
}
public void setMiddle(String middle) {
this.middle = middle;
}
public String getLast() {
return last;
}
public void setLast(String last) {
this.last = last;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
public static Map<String,Boolean> fieldSerialisationCount = new HashMap<>();
public static void main(String[] args) throws Exception {
Contact contact = new Contact("Bob", null, "Barker", null);
ObjectMapper mapper = new ObjectMapper();
FilterProvider filters = new SimpleFilterProvider().addFilter("myFilter", new SimpleBeanPropertyFilter() {
@Override
protected boolean include(BeanPropertyWriter writer) {
return super.include(writer) && isAuthed(writer);
}
@Override
protected boolean include(PropertyWriter writer) {
return super.include(writer) && isAuthed(writer);
}
private boolean isAuthed(PropertyWriter writer) {
if (!writer.getMember().hasAnnotation(JsonAuth.class)) {
return true;
} else {
return fieldSerialisationCount.compute(writer.getName(), (n, b) -> b == null ? true : !b); // check auth here
}
}
});
mapper.setFilterProvider(filters);
ObjectWriter writer = mapper.writer(filters).withDefaultPrettyPrinter();
System.out.println(
writer.writeValueAsString(contact)
);
System.out.println(
writer.writeValueAsString(contact)
);
System.out.println(
writer.writeValueAsString(contact)
);
}
}
It serialises annotated fields every other time, just as an example of a filter using persistent state.
Please let me know whether this works for you.
By the way, I agree that Jackson has the problem you describe, and I don't know how to solve it, so this is a work-around rather than an answer to your original question.
Related Topics
Java Generics with a Class & an Interface - Together
How to Map a Composite Key with JPA and Hibernate
How to Turn Off the Eclipse Code Formatter for Certain Sections of Java Code
Scale the Imageicon Automatically to Label Size
How to Deploy a Javafx 11 Desktop Application with a Jre
Java, Classpath, Classloading => Multiple Versions of the Same Jar/Project
Should I Initialize Variable Within Constructor or Outside Constructor
Java.Lang.Classnotfoundexception When Running in Intellij Idea
What Is the Default Initialization of an Array in Java
How to Get Current Moment in Iso 8601 Format with Date, Hour, and Minute
How to Specify Jackson to Only Use Fields - Preferably Globally
How to Have an Autocomplete Using Jtextfield and a Jlist
Concatenating Null Strings in Java
How to Convert Byte Size into a Human-Readable Format in Java
How to Print Binary Tree Diagram in Java
What Is the Easiest/Best/Most Correct Way to Iterate Through the Characters of a String in Java