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
Related
I'm trying to create a generic Jackon polymorphic serializer that is able to serialize and deserialize to and from JSON with this format including the fqcn of the class of the object:
{
"fqcn": "full qualified class name of the object",
"data": "serialized object"
}
This wrapper should be applied to any object, so for example this will be the JSON representation of a HashMap> object:
{
"fqcn": "java.util.HashMap",
"data": {
"key1": {
"fqcn": "java.util.ArrayList",
"data": [
{
"fqcn": "java.lang.String",
"data": "value1"
},
{
"fqcn": "java.lang.String",
"data": "value2"
}
]
},
"key2": {
...
}
}
}
I could use a MixIn annotation all objects with #JsonTypeInfo
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.WRAPPER_OBJECT)
public interface ObjMixin {
}
---
ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(Object.class, ObjMixin.class);
However, the format does not match with the required format: {"fqcn": ..., "data": ...}
I've also tried to register a StdConverter to convert any objects to a wrapper like this:
public class ObjectWrapper {
private String fqcn;
private Object data;
public ObjectWrapper(Object obj) {
this.fqcn = obj.getClass.getCanonicalName();
this.data = obj;
}
}
However it is not possible to create a StdDelegatingSerializer for Object.class.
With a custom StdSerializer like the following I am getting StackOverflowError:
#Override
public void serialize(Object obj, JsonGenerator jsonGen, SerializerProvider serializerProvider) throws IOException {
jsonGen.writeStartObject();
jsonGen.writeStringField("fqcn", obj.getClass().getCanonicalName());
jsonGen.writeFieldName("data");
if (obj instanceof Iterable) {
jsonGen.writeStartArray();
// Recursive serialization of all elements in the iterable
jsonGen.writeEndArray();
} else if (obj instanceof Map) {
jsonGen.writeStartObject();
// Recursive serialization of all elements in the map
jsonGen.writeEndObject();
} else {
// Infinite recursion here because I'm defining this serializer for Object.class
serializerProvider.defaultSerializeValue(obj, jsonGen);
}
}
Does anyone know any other solution to be able to achieve this?
You could use a custom serializer and custom serializer provider to wrap every object you want to serialize into this wrapper object (EDIT: that did not work recusrively, updated the code to not use the wrapper object but write the fields instead):
public class FQCNTest {
#Test
public void doTest() throws JsonProcessingException {
final ObjectMapper om = getObjectMapper();
final Object obj = getTestObject();
final String json = om.writeValueAsString(obj);
System.out.println(json); // {"fqcn":"java.util.HashMap","data":{"k":{"fqcn":"java.lang.String","data":"v"}}}
final Object obj2 = getTestValue();
final String json2 = om.writeValueAsString(obj2);
System.out.println(json2); // {"fcqn":"java.lang.String","data":"hello"}
final Object obj3 = null;
final String json3 = om.writeValueAsString(obj3);
System.out.println(json3); // null
}
private ObjectMapper getObjectMapper() {
final ObjectMapper mapper = new ObjectMapper();
final SerializerProvider sp = mapper.getSerializerProviderInstance();
mapper.setSerializerProvider(new CustomSerializerProvider(sp, mapper.getSerializerFactory()));
return mapper;
}
private Object getTestObject() {
final HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("k", "v");
return hashMap;
}
private Object getTestValue() {
return "hello";
}
}
class CustomSerializerProvider extends DefaultSerializerProvider {
private final SerializerProvider defaultInstance;
protected CustomSerializerProvider(final SerializerProvider defaultInstance, final SerializerFactory f) {
super(defaultInstance, defaultInstance.getConfig(), f);
this.defaultInstance = defaultInstance;
}
#Override
public WritableObjectId findObjectId(final Object forPojo, final ObjectIdGenerator<?> generatorType) {
return defaultInstance.findObjectId(forPojo, generatorType);
}
#Override
public JsonSerializer<Object> serializerInstance(final Annotated annotated, final Object serDef) throws JsonMappingException {
return new CustomSerializer();
}
#Override
public Object includeFilterInstance(final BeanPropertyDefinition forProperty, final Class<?> filterClass) {
try {
return defaultInstance.includeFilterInstance(forProperty, filterClass);
} catch (final JsonMappingException e) {
throw new RuntimeException(e);
}
}
#Override
public boolean includeFilterSuppressNulls(final Object filter) throws JsonMappingException {
return defaultInstance.includeFilterSuppressNulls(filter);
}
#Override
public DefaultSerializerProvider createInstance(final SerializationConfig config, final SerializerFactory jsf) {
return this;
}
#Override
public void serializeValue(final JsonGenerator gen, final Object value) throws IOException {
new CustomSerializer().serialize(value, gen, this);
}
#Override
public void serializeValue(final JsonGenerator gen, final Object value, final JavaType rootType) throws IOException {
super.serializeValue(gen, value, rootType);
}
#Override
public void serializeValue(final JsonGenerator gen, final Object value, final JavaType rootType, final JsonSerializer<Object> ser) throws IOException {
super.serializeValue(gen, value, rootType, ser);
}
}
class CustomSerializer extends StdSerializer<Object> {
protected CustomSerializer() {
super(Object.class);
}
#Override
public void serialize(final Object value, final JsonGenerator gen, final SerializerProvider provider) throws IOException {
if (value == null) {
provider.defaultSerializeValue(value, gen);
return;
}
final Class<?> clazz = value.getClass();
final JsonSerializer<Object> serForClazz = provider.findValueSerializer(clazz);
gen.writeStartObject();
gen.writeStringField("fqcn", clazz.getCanonicalName());
gen.writeFieldName("data");
if (value instanceof Iterable) {
gen.writeStartArray();
for (final Object e : ((Iterable<?>) value)) {
final JsonSerializer<Object> ser = new CustomSerializer();
ser.serialize(e, gen, provider);
}
gen.writeEndArray();
} else if (value instanceof Map) {
gen.writeStartObject();
// Recursive serialization of all elements in the map
for (final Map.Entry<?, ?> e : ((Map<?, ?>) value).entrySet()) {
final String key = e.getKey().toString(); // need to handle keys better
final Object mapValue = e.getValue();
gen.writeFieldName(key);
final JsonSerializer<Object> ser = new CustomSerializer();
ser.serialize(mapValue, gen, provider);
}
gen.writeEndObject();
} else {
serForClazz.serialize(value, gen, provider);
}
gen.writeEndObject();
}
}
Note: this code may contain too much stuff that is not necessary, I just took it far enough to make it work for the specific example. (and did not test deserialization, that may be a totally different thing)
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());
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);
}
}
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.