If I use jackson-dataformat-xml to serialise an ArrayList of JsonNode it produces the following:
<ArrayList><item>...</item><item>...</item></ArrayList>
I'd like to have it read:
<events><event>...</event><event>...</event></events>
I've tried using a custom serialiser and does give me the <event/> tags I want but it also still wraps everything in an <ArrayList><item></item></ArrayList>
public class ArrayListSerializer extends JsonSerializer<ArrayList<JsonNode>> {
#Override
public void serialize(ArrayList<JsonNode> value, JsonGenerator gen,
SerializerProvider serializers)
throws IOException, JsonProcessingException {
gen.writeStartObject();
for (JsonNode node : value) {
gen.writeObjectField("event", node);
}
gen.writeEndObject();
}
#Override
public Class<ArrayList<JsonNode>> handledType() {
#SuppressWarnings("unchecked")
Class<ArrayList<JsonNode>> typeClass = (Class<ArrayList<JsonNode>>)(Class<?>)List.class;
return typeClass;
}
}
Any suggestions? Thanks.
You can also consider defining the XML element names using annotations. Here is an example:
public class JacksonXmlArray {
static class Document {
#JsonProperty
#JacksonXmlElementWrapper(localName = "events")
#JacksonXmlProperty(localName = "event")
List<String> events = Arrays.asList("a", "b");
}
public static void main(String[] args) throws JsonProcessingException {
final XmlMapper xmlMapper = new XmlMapper();
System.out.println(xmlMapper.writeValueAsString(new Document()));
}
}
Output:
<Document xmlns=""><events><event>a</event><event>b</event></events></Document>
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();
}
}
I'm looking for a way to (de-)serialize a List of items without using Annotations in Jackson. Is this possible? What I'm doing up to now is trying to replace the <item>-tag with a tag telling about the item's class, but no avail. And even if this worked, I'm not sure whether Jackson would offer a way to process this tag information.
To give a better of what I'm aiming at, here's a sample:
public class JacksonTest {
private static class ListElement {
private boolean value;
// getters, setters, constructors omitted
}
#Test
public void testDeSerialization() throws Exception {
final List<ListElement> existing = Arrays.asList(new ListElement(true));
final ObjectMapper mapper = new XmlMapper();
final JavaType listJavaType = mapper.getTypeFactory().constructCollectionType(List.class, ListElement.class);
final String listString = mapper.writerFor(listJavaType).writeValueAsString(existing);
System.out.println(listString);
// "<List><item><value>true</value></item></List>"
}
}
So, the result is <List><item><value>true</value></item></List>, while I want the <item>-tag to be replaced with the (qualified) class name or offering a type-attribute.
Of course, even this would not help if there's no way in Jackson to process this class name.
Do I have reached a dead end here or is there a way to go?
You can define your own JsonSerializer (also used for XML) and add it to a JacksonXmlModule.
ToXmlGenerator has a setNextName function that allows you to override the default item name
private class MyListSerializer extends JsonSerializer<List> {
#Override
public void serialize(List list, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
for (Object obj : list) {
if (jsonGenerator instanceof ToXmlGenerator) {
ToXmlGenerator xmlGenerator = (ToXmlGenerator) jsonGenerator;
String className = obj.getClass().getSimpleName();
xmlGenerator.setNextName(new QName(className));
}
jsonGenerator.writeObject(obj);
// this is overridden at the next iteration
// and ignored at the last
jsonGenerator.writeFieldName("dummy");
}
}
#Override
public Class<List> handledType() {
return List.class;
}
}
#Test
public void testDeSerialization() throws Exception {
final List<ListElement> existing = Arrays.asList(new ListElement(true));
JacksonXmlModule module = new JacksonXmlModule();
module.addSerializer(new MyListSerializer());
final ObjectMapper mapper = new XmlMapper(module);
final JavaType listJavaType = mapper.getTypeFactory().constructCollectionType(List.class, ListElement.class);
final ObjectWriter writer = mapper.writerFor(listJavaType);
final String listString = writer.writeValueAsString(existing);
System.out.println(listString);
// "<List><ListElement><value>true</value></ListElement></List>"
}
Okay, after some tinkering and debugging with Evertude's proposal I've figured out a solution. I'm not really happy with the serialization part and honestly I don't know why I was supposed to do it this way. When debugging I've noticed that XmlGenerator::setNextName is required to be called once but does not have any effect on the next call, so I had to implement a switch there and set the field name for the next item in the loop directly.
I'ld be glad if somebody has an idea what I'm doing wrong, but at least my attempt is working for now:
#Test
public void testDeSerialization() throws Exception {
final List<ListElement> existing = Arrays.asList(new ListElement(true), new ListElement(false));
JacksonXmlModule module = new JacksonXmlModule();
module.addSerializer(new MyListSerializer());
final ObjectMapper mapper = new XmlMapper(module);
final JavaType listJavaType = mapper.getTypeFactory().constructCollectionType(List.class, ListElement.class);
final ObjectWriter writer = mapper.writerFor(listJavaType);
final String listString = writer.writeValueAsString(existing);
module.addDeserializer(List.class, new MyListDeserializer());
List<ListElement> deserialized = mapper.readValue(listString, List.class);
assertEquals(existing, deserialized); // provided there're proper hash() and equals() methods
}
private class MyListSerializer extends JsonSerializer<List> {
#Override
public void serialize(List list, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
boolean done = false;
for (Object obj : list) {
if (jsonGenerator instanceof ToXmlGenerator) {
ToXmlGenerator xmlGenerator = (ToXmlGenerator) jsonGenerator;
String className = obj.getClass().getSimpleName();
// weird switch
if (!done) xmlGenerator.setNextName(new QName(className));
else jsonGenerator.writeFieldName(className);
done = true;
}
jsonGenerator.writeObject(obj);
}
}
#Override
public Class<List> handledType() {
return List.class;
}
}
private class MyListDeserializer extends JsonDeserializer<List> {
#Override
public List deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
List<Object> items = new ArrayList<>();
JsonToken nextToken;
while ((nextToken = p.nextToken()) != JsonToken.END_OBJECT) {
String currentName = p.currentName();
try {
String className = "my.test.project.JacksonCustomSerializer$" + currentName;
Class<?> loadClass = getClass().getClassLoader().loadClass(className);
p.nextToken();
items.add(p.readValueAs(loadClass));
} catch (ClassNotFoundException e) {
// some handling
}
}
return items;
}
#Override
public Class<List> handledType() {
return List.class;
}
}
I have a nested Map<StructureNode, Map<String, String>> for which I need a custom key serializer & deserializer (StructureNode contains references to other objects which are needed to function as key for this map). I used the following method for this:
Jackson Modules for Map Serialization
Giving the following result. Custom Serializer:
public class StructureNodeKeySerializer extends JsonSerializer<StructureNode> {
private static final ObjectMapper mapper = new ObjectMapper();
#Override
public void serialize(StructureNode value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
StringWriter writer = new StringWriter();
mapper.writeValue(writer, value.copyUpwards());
gen.writeFieldName(writer.toString());
}
}
Custom deserializer:
public class StructureNodeKeyDeserializer extends KeyDeserializer {
private static final ObjectMapper mapper = new ObjectMapper();
#Override
public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException {
return mapper.readValue(key, StructureNode.class);
}
}
Usage:
#JsonDeserialize(keyUsing = StructureNodeKeyDeserializer.class) #JsonSerialize(keyUsing = StructureNodeKeySerializer.class)
private Map<StructureNode, String> structureIds;
#JsonDeserialize(keyUsing = StructureNodeKeyDeserializer.class) #JsonSerialize(keyUsing = StructureNodeKeySerializer.class)
private Map<StructureNode, Map<String, String>> metadata;
This correctly serializes a Map<StructureNode, String>, but applied to a nested Map<StructureNode, Map<String, String>>, it gives the following error:
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: java.lang.String cannot be cast to structure.StructureNode
Jackson seems to be using the same custom serialization method for the "sub-map". Is there a good way to solve this problem, without replacing the "sub-map" with another custom (non-Map) object?
You can fix this with
public static class Bean{
#JsonSerialize(using = MapStructureNodeKeySerializer.class)
public Map<StructureNode, Map<String, String>> metadata;
}
And implement your serializer a little bit differently:
public static class MapStructureNodeKeySerializer
extends JsonSerializer<Map<StructureNode, Object>> {
private static final ObjectMapper mapper = new ObjectMapper();
#Override
public void serialize(Map<StructureNode, Object> value, JsonGenerator gen,
SerializerProvider serializers) throws IOException {
gen.writeStartObject();
for(Map.Entry<StructureNode, Object> val: value.entrySet()){
// your custom serialization code here
StringWriter writer = new StringWriter();
mapper.writeValue(writer, val.getKey().copyUpwards());
gen.writeObjectField(writer.toString(), val.getValue());
}
gen.writeEndObject();
}
}
Or if you want to keep keyUsing = StructureNodeKeySerializer.class
public static class Bean{
#JsonSerialize(keyUsing = StructureNodeKeySerializer.class)
public Map<StructureNode, Map<String, String>> metadata;
}
You can implement it like:
public static class StructureNodeKeySerializer extends JsonSerializer {
private static final ObjectMapper mapper = new ObjectMapper();
#Override
public void serialize(Object value, JsonGenerator gen,
SerializerProvider serializers) throws IOException {
if (value instanceof StructureNode){ // <= type of 1-st level Map key
// your custom serialization code here
StringWriter writer = new StringWriter();
mapper.writeValue(writer, ((StructureNode)value).copyUpwards());
gen.writeFieldName(writer.toString());
}else if(value instanceof String){ // <= type of 2-nd level Map key
gen.writeFieldName((String) value);
}
}
}
If you want to serialize it more generically as keySerializer, you can rewrite the else clause as follows
if (value instanceof StructureNode) {
// ...
} else {
serializers
.findKeySerializer(value.class, null)
.serialize(value, gen, serializers);
}
here I have a generic class like :
#JacksonXmlRootElement(localName = "Request")
public class Request<T> {
#JacksonXmlProperty(isAttribute = false,localName="HeaderInfo")
private HeaderInfo headerInfo;
//here I want serialization data use T className
private T data;
}
for example:
I got
Request<User> request; to serialization XML like:
<request><headinfo></headinfo><user></user><request>
Request<SuperUser> request; to serialization XML like:
<request><headinfo></headinfo><SuperUser></SuperUser><request>
ect...
OK I have solve this promblem
code:
public class RequestSerializer extends StdSerializer<Request> {
public RequestSerializer() {
super(Request.class);
}
#Override
public void serialize(Request request, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeStartObject();
jgen.writeObjectField("HeaderInfo", request.getHeaderInfo());
String name = request.getRequest().getClass().getSimpleName();
jgen.writeObjectField(name, request.getRequest());
jgen.writeEndObject();
}
}
JacksonXmlModule module = new JacksonXmlModule();
module.setDefaultUseWrapper(true);
module.addSerializer(Request.class, new RequestSerializer());
XmlMapper xml = new XmlMapper(module);
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?