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();
}
}
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)
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 am having trouble getting jackson to respect my custom JsonDeserializer. The situation is, I have a class MyClass that contains a list of another class, OtherClass, that is outside of my control (so I can't annotate it). This OtherClass class is an interface with multiple implementations. I don't care what the original OtherClass was, I want them to always deserialize as BasicOtherClass.
Here is what I have:
#Getter
public class MyClass {
#JsonProperty("otherclasses")
#JsonSerialize(contentUsing=OtherClassSerializer.class)
#JsonDeserialize(contentUsing=OtherClassDeserializer.class)
private List<OtherClass> otherClasses;
public MyClass(
#JsonProperty("otherclasses")
#JsonDeserialize(contentUsing=OtherClassDeserializer.class)
List<OtherClass> otherClasses) {
this.otherClass = otherClass;
}
}
public static class OtherClassSerializer extends JsonSerializer<OtherClass> {
#Override
public void serialize(OtherClass otherClass, JsonGenerator gen, SerializerProvider serializers)
throws IOException, JsonProcessingException {
gen.writeStartObject();
gen.writeStringField("name", otherClass.getName());
gen.writeStringField("value", otherClass.getValue());
gen.writeEndObject();
}
/** This method is required when default typing is enabled */
#Override
public void serializeWithType(
OtherClass otherClass, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer)
throws JsonProcessingException, IOException {
typeSer.writeTypePrefixForScalar(value, gen, OtherClass.class);
serialize(value, gen, serializers);
typeSer.writeTypeSuffixForScalar(value, gen);
}
}
public static class OtherClassDeserializer extends JsonDeserializer<Header> {
#Override
public Header deserialize(JsonParser p, DeserializationContext ctxt) throws IOException,
JsonProcessingException {
if (p.getCurrentToken() != JsonToken.START_OBJECT) {
throw new IllegalStateException("Failed to parse OtherClass from json");
}
String name = null;
String value = null;
while (p.nextToken() != JsonToken.END_OBJECT) {
String key = p.getText();
p.nextToken();
String val = p.getText();
if (key.equals("name")) {
name = val;
} else if (key.equals("value")) {
value = val;
}
}
return new BasicOtherClass(name, value);
}
}
This is what I am trying to get to work:
ObjectMapper mapper = new ObjectMapper().enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
OtherClass otherClass = new BufferedOtherClass("name value");
MyClass myClass = new MyClass(Lists.newArrayList(otherClass));
String json = mapper.writeValueAsString(myClass);
// json == ["com.bschlenk.MyClass", {"otherclass": ["java.util.ArrayList", [["com.other.OtherClass", {"name": "name", "value", "value"}]]]}]
But when I try to read that json back into MyClass, it fails:
MyClass parsed = mapper.readValue(json, MyClass.class);
// com.fasterxml.jackson.databind.JsonMappingException:
// Can not construct instance of org.apache.http.Header, problem:
// abstract types either need to be mapped to concrete types,
// have custom deserializer,
// or be instantiated with additional type information
This works when I don't have type information enabled. However, it is other code that is serializing MyClass that I don't have control of, and it has type info on.
Is what I am trying to do even possible? Why doesn't mapper.readValue use my custom JsonDeserializer class? Is this by design?
Question How to indicate ObjectMapperthat he should filter object's nested collection by some criteria (field). See explanation via code:
Explanation via code:
I have to convert Container object to JSON. But I want to filter entries collection based on Entry.value field. I mean I want to serialize Container and include only that Entries which value == 1.
public class Container {
List<Entry> entries;
public void setEntries(List<Entry> entries) {
this.entries = entries;
}
}
public class Entry {
int value;
public Entry(int value) {
this.value = value;
}
}
public static void main(String[] args) {
ObjectMapper mapper = new ObjectMapper();
Container container = new Container();
container.setEntries(new LinkedList<Entry>({
{
add(new Entry(1));
add(new Entry(2));
add(new Entry(1));
}
}))
// Now I want to get container object only with two elements in list
mapper.writeValueAsString(container);
}
You can make Entry implement JsonSerializable. In Jackson 2.x it will give:
public class Entry
implements JsonSerializable
{
int value;
public Entry(int value)
{
this.value = value;
}
#Override
public void serialize(final JsonGenerator jgen,
final SerializerProvider provider)
throws IOException
{
// Don't do anything if value is not 1...
if (value != 1)
return;
jgen.writeStartObject();
jgen.writeNumberField("value", 1);
jgen.writeEndObject();
}
#Override
public void serializeWithType(final JsonGenerator jgen,
final SerializerProvider provider, final TypeSerializer typeSer)
throws IOException
{
serialize(jgen, provider);
}
}
Another solution would be to implement a custom JsonSerializer<Entry> and register it before you serialize; it would basically do the same as the above.
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.