The problem is in serialize method (see code section of my question). All looks well till my ComplexGraph class swell to twenty ot thirty fields. When ComplexGraph become a real complex graph, than it's very ugly to serialize each field via jsonGenerator.writeNumberField("id", id).
Question: How can I invoke standard serialization in serialize method? Like:
public class ComplexGraph implements JsonSerializable {
public void serialize(JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
if (isPrivate) return;
//HERE IS THE QUESTION
else jsonGenerator.standardSerialization(this);
}
}
Code:
public class ComplexGraph implements JsonSerializable {
private int id;
private String text;
private boolean isPrivate;
// getters and setters
#Override
public void serialize(JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
if (isPrivate) return;
jsonGenerator.writeStartObject();
//IT'S VERY UGLY WHEN complexGraph HAS MANY DEPENDENCIES!
jsonGenerator.writeNumberField("id", id);
jsonGenerator.writeStringField("text", text);
jsonGenerator.writeEndObject();
}
#Override
public void serializeWithType(JsonGenerator jsonGenerator, SerializerProvider serializerProvider, TypeSerializer typeSerializer) throws IOException, JsonProcessingException {
serialize(jsonGenerator, serializerProvider);
}
}
Related
Is there a way to serialize collection and its elements unwrapped?
For example I want to serialize unwrapped all components:
class Model {
#JsonProperty
#JsonUnwrapped
Collection<Object> components;
Model(Collection<Object> components) {
this.components = components;
}
static class Component1 {
#JsonProperty
String stringValue;
Component1(String stringValue) {
this.stringValue= stringValue;
}
}
static class Component2 {
#JsonProperty
int intValue;
Component2(int intValue) {
this.intValue= intValue;
}
}
public static void main(String[] args) throws JsonProcessingException {
Model model = new Model(Arrays.asList(new Component1("something"), new Component2(42)));
String json = new ObjectMapper().writeValueAsString(model);
System.out.println(json);
}
}
Expected:
{"stringValue":"something","intValue":42}
But actual result is:
{"components":[{"stringValue":"something"},{"intValue":42}]}
Custom serializer might help:
class ModelSerializer extends JsonSerializer<Model> {
#Override
public void serialize(Model model, JsonGenerator generator, SerializerProvider serializers) throws IOException {
generator.writeStartObject();
JsonSerializer<Object> componentSerializer = serializers.findValueSerializer(getClass());
JsonSerializer<Object> unwrappingSerializer = componentSerializer.unwrappingSerializer(NameTransformer.NOP);
unwrappingSerializer.serialize(this, generator, serializers);
generator.writeEndObject();
}
}
I can't see a way to do that without custom serialization. I recommend these 2 serializers:
class ValueSerializer extends JsonSerializer<Object> {
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider sers) throws IOException {
for (Field field : value.getClass().getDeclaredFields()) {
try {
field.setAccessible(true);
gen.writeObjectField(field.getName(), field.get(value));
} catch (IllegalAccessException ignored) {
}
}
}
}
class ModelSerializer extends JsonSerializer<Model> {
#Override
public void serialize(Model model, JsonGenerator gen, SerializerProvider sers) throws IOException {
gen.writeStartObject();
for (Object obj : model.getComponents()) {
gen.writeObject(obj);
}
gen.writeEndObject();
}
}
Notice how we don't call writeStartObject() at ValueSerializer so no extra curly braces from here, neither from writeObjectField. On the other hand in ModelSerializer writheStartObject adds curly braces, and then we dump within them each object in components
You'd also need to annotate serializable classes to use these serializers e.g.
#JsonSerialize(using = ValueSerializer.class)
class Component1 {
#JsonSerialize(using = ValueSerializer.class)
class Component2 {
#JsonSerialize(using = ModelSerializer.class)
class Model {
Not elegant, but work code.
Sure about unique naming of key values
#JsonProperty
#JsonSerialize(using = CollectionSerializer.class)
Collection<Object> components;
static class CollectionSerializer extends JsonSerializer<Object> {
#Override
public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeStartObject();
if (o instanceof Collection) {
Collection c = (Collection) o;
for (Object el : c) {
if (el instanceof Component1) {
jsonGenerator.writeStringField("stringValue", ((Component1) el).stringValue);
}
if (el instanceof Component2) {
jsonGenerator.writeNumberField("intValue", ((Component2) el).intValue);
}
}
}
jsonGenerator.writeEndObject();
}
}
According to this answer:
https://stackoverflow.com/a/43342675/5810648
I wrote such serializer:
public class CustomSerializer extends StdSerializer<Double> implements ContextualSerializer {
private final NAifNull annotation;
public CustomSerializer() {
super(Double.class);
this.annotation = null;
}
public CustomSerializer(NAifNull annotation) {
super(Double.class);
this.annotation = annotation;
}
#Override
public void serialize(Double value, JsonGenerator gen, SerializerProvider provider) throws IOException {
if (annotation != null && value == null) {
gen.writeString("N/A");
} else {
gen.writeNumber(value);
}
}
#Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
NAifNull annotation = property.getAnnotation(NAifNull.class);
return new CustomSerializer(annotation);
}
}
Witch supposed to write string "N/A" if the annotation is present and field is null. But method serialize is called only for not null fields.
Also, I have tried to call setNullValueSerializer:
#Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) {
NAifNull annotation = property.getAnnotation(NAifNull.class);
prov.setNullValueSerializer(new CustomNullSerializer(annotation));
return new CustomSerializer(annotation);
}
With such implementation:
private static class CustomNullSerializer extends JsonSerializer<Object> {
private final NAifNull annotation;
public CustomNullSerializer(NAifNull annotation) {
this.annotation = annotation;
}
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (annotation != null) {
gen.writeString("N/A");
} else {
gen.writeNull();
}
}
}
But no result.
How to handle null fields in such way?
Update
According to discussion:
https://github.com/FasterXML/jackson-databind/issues/2057
prov.setNullValueSerializer(new CustomNullSerializer(annotation));
Is not supposed to be called from CreateContextual method.
Use a BeanSerializerModifier to customize the null serializer for a particular property:
public class CustomBeanSerializerModifier extends BeanSerializerModifier {
#Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config,
BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
for (BeanPropertyWriter beanProperty : beanProperties) {
if (beanProperty.getAnnotation(NAifNull.class) != null) {
beanProperty.assignNullSerializer(new CustomNullSerializer());
}
}
return beanProperties;
}
}
Where #NAifNull and CustomNullSerializer are define as follows:
public class CustomNullSerializer extends JsonSerializer<Object> {
#Override
public void serialize(Object value, JsonGenerator jgen,
SerializerProvider provider) throws IOException {
jgen.writeString("N/A");
}
}
#Target({ ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
#interface NAifNull {
}
Then use it as follows:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new SimpleModule() {
#Override
public void setupModule(SetupContext context) {
super.setupModule(context);
context.addBeanSerializerModifier(new CustomBeanSerializerModifier());
}
});
If I understood you correctly, you want to write "N/A" to generated JSON, if the value is null.
Jackson docs states that value cannot be null. This is because the type parameter is Class object, which is constructed automatically by JVM.
As per this article, I think you could handle null fields with something like
public class CustomNullSerializer extends StdSerializer<Object> {
public CustomNullSerializer() {
this(null);
}
public CustomNullSerializer(Class<Object> t) {
super(t);
}
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString("N/A");
}
}
And then use it with
prov.setNullValueSerializer(new CustomNullSerializer());
Thought I didn't try this myself, but I hope it helps.
UPDATE
Okey, now I had time to try this myself. I got it working with
ObjectMapper mapper...
mapper.getSerializerProvider().setNullValueSerializer(new CustomNullSerializer());
I have two classes. First with field of second class.
class A {
#JsonSerializer(using = CustomBSerializer.class)
private B b;
}
class B {
...
}
And I have two custom serializers:
class CustomBSerializer extends JsonSerializer<B> {
...
}
class CustomASerializer extends JsonSerializer<A> {
#Override
public void serialize(A a, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
jgen.writeStartObject();
//write here
jgen.writeEndObject();
}
}
What method I should add instead of comment in CustomASerializer to write serialized value of field b with CustomBSerializer?
You can use writeObjectField method. Jackson should use default or custom serializer if exists.
Below example:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.IOException;
import java.util.Date;
public class Main {
public static void main(String[] args) throws Exception {
B b = new B();
b.setProperty("Value");
A a = new A();
a.setB(b);
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(a));
}
}
#JsonSerialize(using = ASerializer.class)
class A {
private B b;
public B getB() {
return b;
}
public void setB(B b) {
this.b = b;
}
}
class ASerializer extends JsonSerializer<A> {
#Override
public void serialize(A value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
gen.writeStartObject();
gen.writeStringField("now", new Date().toString());
gen.writeObjectField("b", value.getB());
gen.writeEndObject();
}
}
#JsonSerialize(using = BSerializer.class)
class B {
private String property;
public String getProperty() {
return property;
}
public void setProperty(String property) {
this.property = property;
}
}
class BSerializer extends JsonSerializer<B> {
#Override
public void serialize(B value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
gen.writeStartObject();
gen.writeStringField("p", value.getProperty());
gen.writeEndObject();
}
}
prints:
{
"now" : "Wed Aug 26 22:27:08 CEST 2015",
"b" : {
"p" : "Value"
}
}
You don't always have access to class code, you will not always have the possibility to modify A or B class to put the annotation #JsonSerialize(using = BSerializer.class). In these cases you will need to call CustomBSerializer in CustomASerializer, which was also what the question was asking in first place. This is the way to do it:
class CustomASerializer extends JsonSerializer<A> {
#Override
public void serialize(A a, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
final ObjectMapper mapper = (ObjectMapper)jgen.getCodec();
SimpleModule module = new SimpleModule("CustomBSerializer");
module.addSerializer(B.class, new CustomBSerializer());
mapper.registerModule(module);
jgen.writeStartObject();
mapper.writeValue(jgen, a.getB());
jgen.writeEndObject();
}
}
I'm trying to serialize a String field as a JSON if it contains a JSON object. For this I wrote a custom serializer:
public class TryJsonStringSerializer extends JsonSerializer<String> {
#Override
public void serialize(String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException, JsonProcessingException {
if (value == null) {
return;
}
value = value.trim();
if (value.startsWith("{") && value.endsWith("}")) {
jsonGenerator.writeRaw(value);
} else {
jsonGenerator.writeString(value);
}
}
}
but I get the following error:
org.codehaus.jackson.JsonGenerationException: Can not write a field name, expecting a value
org.codehaus.jackson.impl.JsonGeneratorBase._reportError(JsonGeneratorBase.java:480)
org.codehaus.jackson.impl.Utf8Generator.writeFieldName(Utf8Generator.java:292)
org.codehaus.jackson.map.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:422)
org.codehaus.jackson.map.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:150)
org.codehaus.jackson.map.ser.BeanSerializer.serialize(BeanSerializer.java:112)
org.codehaus.jackson.map.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:446)
org.codehaus.jackson.map.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:150)
org.codehaus.jackson.map.ser.BeanSerializer.serialize(BeanSerializer.java:112)
org.codehaus.jackson.map.ser.std.StdContainerSerializers$IndexedListSerializer.serializeContents(StdContainerSerializers.java:122)
org.codehaus.jackson.map.ser.std.StdContainerSerializers$IndexedListSerializer.serializeContents(StdContainerSerializers.java:71)
org.codehaus.jackson.map.ser.std.AsArraySerializerBase.serialize(AsArraySerializerBase.java:86)
org.codehaus.jackson.map.ser.StdSerializerProvider._serializeValue(StdSerializerProvider.java:610)
org.codehaus.jackson.map.ser.StdSerializerProvider.serializeValue(StdSerializerProvider.java:256)
org.codehaus.jackson.map.ObjectMapper.writeValue(ObjectMapper.java:1613)
...
What will be the best way to achieve this?
public class TryJsonStringSerializer extends JsonSerializer<String> {
private RawSerializer<String> rawSerializer = new RawSerializer<String>(String.class);
private ToStringSerializer stringSerializer = ToStringSerializer.instance;
#Override
public void serialize(String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException, JsonProcessingException {
if (value == null) {
return;
}
value = value.trim();
if (value.startsWith("{") && value.endsWith("}")) {
rawSerializer.serialize(value, jsonGenerator, serializerProvider);
} else {
stringSerializer.serialize(value, jsonGenerator, serializerProvider);
}
}
}
You may want to use special annotation: #JsonRawValue, see docs
I want to configure a Jackson deserializer that act differently depending on the target type of the annotated field.
public class Car {
#JsonSerialize(using=IdSerializer.class)
#JsonDeserialize(using=IdDeserializer.class)
String id
}
public class Bus {
#JsonSerialize(using=IdSerializer.class)
#JsonDeserialize(using=IdDeserializer.class)
Id id
}
Jackson serializers know the type from which it is converting data, so this is working:
public class IdSerializer extends JsonSerializer<Object> {
#Override
public void serialize(Object value, JsonGenerator jsonGen, SerializerProvider provider) throws IOException {
// value is the annotated field class
if(value instanceof String)
jsonGen.writeObject(...);
else if (value instanceof Id)
jsonGen.writeObject(...);
else
throw new IllegalArgumentException();
}
}
Jackson deserializers seem to do not know the target type into which it will convert data:
public class IdDeserializer extends JsonDeserializer<Object> {
#Override
public Object deserialize(JsonParser jp, DeserializationContext context) throws IOException {
// what is the annotated field class?
}
}
In the serializer, you could add extra information about the type that will help you during deserialization.
Building from your posted IdSerializer...
public class IdSerializer extends JsonSerializer<Object> {
#Override
public void serialize(Object value, JsonGenerator jsonGen, SerializerProvider provider) throws IOException {
// value is the annotated field class
if(value instanceof String){
jsonGen.writeStartObject();
jsonGen.writeFieldName("id");
jsonGen.writeObject(value);
jsonGen.writeFieldName("type");
jsonGen.writeString("String");
jsonGen.writeEndObject();
}
else if (value instanceof Id){
Id id = (Id) value;
jsonGen.writeStartObject();
jsonGen.writeFieldName("id");
jsonGen.writeString(id.getStuff());
jsonGen.writeFieldName("type");
jsonGen.writeString("Id");
jsonGen.writeEndObject();
}
else{
throw new IllegalArgumentException();
}
}
}
In your deserializer, you can parse this 'type' field and return an Object of the proper
type.