I have restlet web service which returns response as xml. I'm using Jackson as binder.
below is class I'm returning.
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class ApiResponse<T> implements Serializable {
/**
*
*/
private static final long serialVersionUID = -2736991050157565598L;
private int responseCode;
private String reponseMessage;
private List<T> body = new ArrayList<T>();
public int getResponseCode() {
return responseCode;
}
public void setResponseCode(int responseCode) {
this.responseCode = responseCode;
}
public String getReponseMessage() {
return reponseMessage;
}
public void setReponseMessage(String reponseMessage) {
this.reponseMessage = reponseMessage;
}
public List<T> getBody() {
return body;
}
public void setBody(List<T> body) {
this.body = body;
}
}
And below is response of the service. Everything is almost good except that it puts as property names for nested objects the same as parents. It shows body for nested tag names but I expect it to be T template. Any ideas?
<ApiResponse>
<responseCode>1</responseCode>
<reponseMessage />
<body>
<body>
<reportId>1</reportId>
<reportName>name1</reportName>
</body>
<body>
<reportId>2</reportId>
<reportName>name2</reportName>
</body>
</body>
</ApiResponse>
This is the default serialization with Jackson. However you can leverage custom serializer to improve this. This feature allows you to have the hand on the generated content within Jackson for a specific class. You can override the default strategy with your own and configure in a very fine manner what will be created.
Below a sample of such entity that generates content for the class SomeBean:
public class SomeBeanSerializer extends JsonSerializer<SomeBean> {
#Override
public void serialize(SomeBean bean, JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonProcessingException {
jgen.writeStartObject();
// Fields
jgen.writeNumberField("id", bean.getId());
(...)
// Link
String href = (...)
HypermediaLink linkToSelf = new HypermediaLink();
linkToSelf.setHref(href + bean.getId());
linkToSelf.setRel("self");
jgen.writeObjectField("hypermediaLink", linkToSelf);
jgen.writeEndObject();
}
}
Here is the way to configure this within Restlet:
JacksonConverter jacksonConverter = getRegisteredJacksonConverter();
if (jacksonConverter != null) {
ObjectMapper objectMapper = jacksonConverter.getObjectMapper();
SimpleModule module = new SimpleModule("MyModule", new Version(1, 0, 0, null));
module.addSerializer(SomeBean.class, new SomeBeanSerializer());
objectMapper.registerModule(module);
}
This link could help you to see how to configure the Jackson converter of Restlet: https://templth.wordpress.com/2015/02/23/optimizing-restlet-server-applications/. It provides the content of the method getRegisteredJacksonConverter.
Edited: with version 2.3 of Restlet, something changes at this level. The object mapper is now brought by the JacksonRepresentation instead of the JacksonConverter itself. The object mapper is now instantiated for each representation of this kind. This means that you need to sub class these two elements to configure the custom serializer.
Here is the code of the class CustomJacksonRepresentation:
public class CustomJacksonRepresentation<T>
extends JacksonRepresentation<T> {
#Override
public ObjectMapper getObjectMapper() {
if (this.objectMapper == null) {
this.objectMapper = createObjectMapper();
SimpleModule module = new SimpleModule("MyModule",
new Version(1, 0, 0, null));
module.addSerializer(SomeBean.class,
new SomeBeanSerializer());
objectMapper.registerModule(module);
}
return this.objectMapper;
}
}
Here is the code of the class CustomJacksonConverter:
public class CustomJacksonConverter
extends JacksonConverter {
protected <T> JacksonRepresentation<T> create(
MediaType mediaType, T source) {
return new CustomJacksonRepresentation<T>(
mediaType, source);
}
protected <T> JacksonRepresentation<T> create(
Representation source, Class<T> objectClass) {
return new CustomJacksonRepresentation<T>(
source, objectClass);
}
}
This implemented, you need to replace the existing jackson converter that is automatically registered by Restlet. Here is the code to do that:
// Looking for the registered jackson converter
JacksonConverter jacksonConverter = null;
List<ConverterHelper> converters
= Engine.getInstance().getRegisteredConverters();
for (ConverterHelper converterHelper : converters) {
if (converterHelper instanceof JacksonConverter) {
jacksonConverter = (JacksonConverter) converterHelper;
break;
}
}
// converters
Engine.getInstance().getRegisteredConverters().remove(
jacksonConverter);
CustomJacksonConverter customJacksonConverter
= new CustomJacksonConverter();
Engine.getInstance().getRegisteredConverters().add(
customJacksonConverter);
You can notice that the way to manage converters will be refactored in the version 3 of Restlet to make things more convenient to configure! ;-)
Hope it helps you,
Thierry
Related
I am trying to include raw JSON inside a Java object when the object is (de)serialized using Jackson. In order to test this functionality, I wrote the following test:
public static class Pojo {
public String foo;
#JsonRawValue
public String bar;
}
#Test
public void test() throws JsonGenerationException, JsonMappingException, IOException {
String foo = "one";
String bar = "{\"A\":false}";
Pojo pojo = new Pojo();
pojo.foo = foo;
pojo.bar = bar;
String json = "{\"foo\":\"" + foo + "\",\"bar\":" + bar + "}";
ObjectMapper objectMapper = new ObjectMapper();
String output = objectMapper.writeValueAsString(pojo);
System.out.println(output);
assertEquals(json, output);
Pojo deserialized = objectMapper.readValue(output, Pojo.class);
assertEquals(foo, deserialized.foo);
assertEquals(bar, deserialized.bar);
}
The code outputs the following line:
{"foo":"one","bar":{"A":false}}
The JSON is exactly how I want things to look. Unfortunately, the code fails with an exception when attempting to read the JSON back in to the object. Here is the exception:
org.codehaus.jackson.map.JsonMappingException: Can not deserialize instance of java.lang.String out of START_OBJECT token
at [Source: java.io.StringReader#d70d7a; line: 1, column: 13] (through reference chain: com.tnal.prism.cobalt.gather.testing.Pojo["bar"])
Why does Jackson function just fine in one direction but fail when going the other direction? It seems like it should be able to take its own output as input again. I know what I'm trying to do is unorthodox (the general advice is to create an inner object for bar that has a property named A), but I don't want to interact with this JSON at all. My code is acting as a pass-through for this code -- I want to take in this JSON and send it back out again without touching a thing, because when the JSON changes I don't want my code to need modifications.
Thanks for the advice.
EDIT: Made Pojo a static class, which was causing a different error.
#JsonRawValue is intended for serialization-side only, since the reverse direction is a bit trickier to handle. In effect it was added to allow injecting pre-encoded content.
I guess it would be possible to add support for reverse, although that would be quite awkward: content will have to be parsed, and then re-written back to "raw" form, which may or may not be the same (since character quoting may differ).
This for general case. But perhaps it would make sense for some subset of problems.
But I think a work-around for your specific case would be to specify type as 'java.lang.Object', since this should work ok: for serialization, String will be output as is, and for deserialization, it will be deserialized as a Map. Actually you might want to have separate getter/setter if so; getter would return String for serialization (and needs #JsonRawValue); and setter would take either Map or Object. You could re-encode it to a String if that makes sense.
Following #StaxMan answer, I've made the following works like a charm:
public class Pojo {
Object json;
#JsonRawValue
public String getJson() {
// default raw value: null or "[]"
return json == null ? null : json.toString();
}
public void setJson(JsonNode node) {
this.json = node;
}
}
And, to be faithful to the initial question, here is the working test:
public class PojoTest {
ObjectMapper mapper = new ObjectMapper();
#Test
public void test() throws IOException {
Pojo pojo = new Pojo("{\"foo\":18}");
String output = mapper.writeValueAsString(pojo);
assertThat(output).isEqualTo("{\"json\":{\"foo\":18}}");
Pojo deserialized = mapper.readValue(output, Pojo.class);
assertThat(deserialized.json.toString()).isEqualTo("{\"foo\":18}");
// deserialized.json == {"foo":18}
}
}
I was able to do this with a custom deserializer (cut and pasted from here)
package etc;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
/**
* Keeps json value as json, does not try to deserialize it
* #author roytruelove
*
*/
public class KeepAsJsonDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
TreeNode tree = jp.getCodec().readTree(jp);
return tree.toString();
}
}
Use it by annotating the desired member like this:
#JsonDeserialize(using = KeepAsJsonDeserializer.class)
private String value;
#JsonSetter may help. See my sample ('data' is supposed to contain unparsed JSON):
class Purchase
{
String data;
#JsonProperty("signature")
String signature;
#JsonSetter("data")
void setData(JsonNode data)
{
this.data = data.toString();
}
}
This is a problem with your inner classes. The Pojo class is a non-static inner class of your test class, and Jackson cannot instantiate that class. So it can serialize, but not deserialize.
Redefine your class like this:
public static class Pojo {
public String foo;
#JsonRawValue
public String bar;
}
Note the addition of static
Adding to Roy Truelove's great answer, this is how to inject the custom deserialiser in response to appearance of #JsonRawValue:
import com.fasterxml.jackson.databind.Module;
#Component
public class ModuleImpl extends Module {
#Override
public void setupModule(SetupContext context) {
context.addBeanDeserializerModifier(new BeanDeserializerModifierImpl());
}
}
import java.util.Iterator;
import com.fasterxml.jackson.annotation.JsonRawValue;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
public class BeanDeserializerModifierImpl extends BeanDeserializerModifier {
#Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
Iterator<SettableBeanProperty> it = builder.getProperties();
while (it.hasNext()) {
SettableBeanProperty p = it.next();
if (p.getAnnotation(JsonRawValue.class) != null) {
builder.addOrReplaceProperty(p.withValueDeserializer(KeepAsJsonDeserialzier.INSTANCE), true);
}
}
return builder;
}
}
This easy solution worked for me:
public class MyObject {
private Object rawJsonValue;
public Object getRawJsonValue() {
return rawJsonValue;
}
public void setRawJsonValue(Object rawJsonValue) {
this.rawJsonValue = rawJsonValue;
}
}
So I was able to store raw value of JSON in rawJsonValue variable and then it was no problem to deserialize it (as object) with other fields back to JSON and send via my REST. Using #JsonRawValue didnt helped me because stored JSON was deserialized as String, not as object, and that was not what I wanted.
This even works in a JPA entity:
private String json;
#JsonRawValue
public String getJson() {
return json;
}
public void setJson(final String json) {
this.json = json;
}
#JsonProperty(value = "json")
public void setJsonRaw(JsonNode jsonNode) {
// this leads to non-standard json, see discussion:
// setJson(jsonNode.toString());
StringWriter stringWriter = new StringWriter();
ObjectMapper objectMapper = new ObjectMapper();
JsonGenerator generator =
new JsonFactory(objectMapper).createGenerator(stringWriter);
generator.writeTree(n);
setJson(stringWriter.toString());
}
Ideally the ObjectMapper and even JsonFactory are from the context and are configured so as to handle your JSON correctly (standard or with non-standard values like 'Infinity' floats for example).
Here is a full working example of how to use Jackson modules to make #JsonRawValue work both ways (serialization and deserialization):
public class JsonRawValueDeserializerModule extends SimpleModule {
public JsonRawValueDeserializerModule() {
setDeserializerModifier(new JsonRawValueDeserializerModifier());
}
private static class JsonRawValueDeserializerModifier extends BeanDeserializerModifier {
#Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
builder.getProperties().forEachRemaining(property -> {
if (property.getAnnotation(JsonRawValue.class) != null) {
builder.addOrReplaceProperty(property.withValueDeserializer(JsonRawValueDeserializer.INSTANCE), true);
}
});
return builder;
}
}
private static class JsonRawValueDeserializer extends JsonDeserializer<String> {
private static final JsonDeserializer<String> INSTANCE = new JsonRawValueDeserializer();
#Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return p.readValueAsTree().toString();
}
}
}
Then you can register the module after creating the ObjectMapper:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JsonRawValueDeserializerModule());
String json = "{\"foo\":\"one\",\"bar\":{\"A\":false}}";
Pojo deserialized = objectMapper.readValue(json, Pojo.class);
I had the exact same issue.
I found the solution in this post :
Parse JSON tree to plain class using Jackson or its alternatives
Check out the last answer.
By defining a custom setter for the property that takes a JsonNode as parameter and calls the toString method on the jsonNode to set the String property, it all works out.
Using an object works fine both ways... This method has a bit of overhead deserializing the raw value in two times.
ObjectMapper mapper = new ObjectMapper();
RawJsonValue value = new RawJsonValue();
value.setRawValue(new RawHello(){{this.data = "universe...";}});
String json = mapper.writeValueAsString(value);
System.out.println(json);
RawJsonValue result = mapper.readValue(json, RawJsonValue.class);
json = mapper.writeValueAsString(result.getRawValue());
System.out.println(json);
RawHello hello = mapper.readValue(json, RawHello.class);
System.out.println(hello.data);
RawHello.java
public class RawHello {
public String data;
}
RawJsonValue.java
public class RawJsonValue {
private Object rawValue;
public Object getRawValue() {
return rawValue;
}
public void setRawValue(Object value) {
this.rawValue = value;
}
}
I had a similar problem, but using a list with a lot of JSON itens (List<String>).
public class Errors {
private Integer status;
private List<String> jsons;
}
I managed the serialization using the #JsonRawValue annotation. But for deserialization I had to create a custom deserializer based on Roy's suggestion.
public class Errors {
private Integer status;
#JsonRawValue
#JsonDeserialize(using = JsonListPassThroughDeserialzier.class)
private List<String> jsons;
}
Below you can see my "List" deserializer.
public class JsonListPassThroughDeserializer extends JsonDeserializer<List<String>> {
#Override
public List<String> deserialize(JsonParser jp, DeserializationContext cxt) throws IOException, JsonProcessingException {
if (jp.getCurrentToken() == JsonToken.START_ARRAY) {
final List<String> list = new ArrayList<>();
while (jp.nextToken() != JsonToken.END_ARRAY) {
list.add(jp.getCodec().readTree(jp).toString());
}
return list;
}
throw cxt.instantiationException(List.class, "Expected Json list");
}
}
I know how to use a custom serializer in Jackson (by extending JsonSerializer), but I want the default serializer to work for all fields, except for just 1 field, which I want to override using the custom serializer.
Annotations are not an option, because I am serializing a generated class (from Thrift).
How do I specify only certain fields to be overridden when writing a custom jackson serializer?
Update:
Here's the class I want to serialize:
class Student {
int age;
String firstName;
String lastName;
double average;
int numSubjects
// .. more such properties ...
}
The above class has many properies, most of which use native types. I want to just override a few properties in the custom serializer and let Jackson deal with the rest as usual. For e.g. I just want to convert the "age" field to a custom output.
Assuming your Target class is
public class Student {
int age;
String firstName;
String lastName;
double average;
int numSubjects;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public double getAverage() {
return average;
}
public void setAverage(double average) {
this.average = average;
}
public int getNumSubjects() {
return numSubjects;
}
public void setNumSubjects(int numSubjects) {
this.numSubjects = numSubjects;
}
}
You need to write a custom serializer as given below
public class MyCustomSerializer extends JsonSerializer<Student> {
#Override
public void serialize(Student value, JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonProcessingException {
if (value != null) {
jgen.writeStartObject();
jgen.writeStringField("age", "Age: " + value.getAge()); //Here a custom way to render age field is used
jgen.writeStringField("firstName", value.getFirstName());
jgen.writeStringField("lastName", value.getLastName());
jgen.writeNumberField("average", value.getAverage());
jgen.writeNumberField("numSubjects", value.getNumSubjects());
//Write other properties
jgen.writeEndObject();
}
}
}
then add it to the ObjectMapper
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule("custom",
Version.unknownVersion());
module.addSerializer(Student.class, new MyCustomSerializer());
mapper.registerModule(module);
then use it like
Student s = new Student();
s.setAge(2);
s.setAverage(3.4);
s.setFirstName("first");
s.setLastName("last");
s.setNumSubjects(3);
StringWriter sw = new StringWriter();
mapper.writeValue(sw, s);
System.out.println(sw.toString());
It will produce a o/p like
{"age":"Age:
2","firstName":"first","lastName":"last","average":3.4,"numSubjects":3}
Just because you can not modify classes DOES NOT mean you could not use annotations: just use mix-in annotations. See this blog entry for example (or google for more with "jackson mixin annotations") for how to use this.
I have specifically used Jackson with protobuf- and thrift-generated classes, and they work pretty well. For earlier Thrift versions, I had to disable discovery of "is-setters", methods Thrift generates to see if a specific property has been explicitly set, but otherwise things worked fine.
I faced the same issue, and I solved it with CustomSerializerFactory.
This approach allows you to ignore some specific field for either for all objects, or for specific types.
public class EntityCustomSerializationFactory extends CustomSerializerFactory {
//ignored fields
private static final Set<String> IGNORED_FIELDS = new HashSet<String>(
Arrays.asList(
"class",
"value",
"some"
)
);
public EntityCustomSerializationFactory() {
super();
}
public EntityCustomSerializationFactory(Config config) {
super(config);
}
#Override
protected void processViews(SerializationConfig config, BeanSerializerBuilder builder) {
super.processViews(config, builder);
//ignore fields only for concrete class
//note, that you can avoid or change this check
if (builder.getBeanDescription().getBeanClass().equals(Entity.class)){
//get original writer
List<BeanPropertyWriter> originalWriters = builder.getProperties();
//create actual writers
List<BeanPropertyWriter> writers = new ArrayList<BeanPropertyWriter>();
for (BeanPropertyWriter writer: originalWriters){
String propName = writer.getName();
//if it isn't ignored field, add to actual writers list
if (!IGNORED_FIELDS.contains(propName)){
writers.add(writer);
}
}
builder.setProperties(writers);
}
}
}
And afterwards you can use it something like the following:
objectMapper.setSerializerFactory(new EntityCustomSerializationFactory());
objectMapper.writeValueAsString(new Entity());//response will be without ignored fields
In case you don't want to pollute your model with annotations, you could use mixins.
ObjectMapper mapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.setMixInAnnotation(Student.class, StudentMixin.class);
mapper.registerModule(simpleModule);
And you want to override id field for example:
public abstract class StudentMixin {
#JsonSerialize(using = StudentIdSerializer.class)
public String id;
}
Do whatever you need with the field:
public class StudentIdSerializer extends JsonSerializer<Integer> {
#Override
public void serialize(Integer integer, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(String.valueOf(integer * 2));
}
}
with the help of #JsonView we can decide fields of model classes to serialize which satisfy the minimal criteria ( we have to define the criteria) like we can have one core class with 10 properties but only 5 properties can be serialize which are needful for client only
Define our Views by simply creating following class:
public class Views
{
static class Android{};
static class IOS{};
static class Web{};
}
Annotated model class with views:
public class Demo
{
public Demo()
{
}
#JsonView(Views.IOS.class)
private String iosField;
#JsonView(Views.Android.class)
private String androidField;
#JsonView(Views.Web.class)
private String webField;
// getters/setters
...
..
}
Now we have to write custom json converter by simply extending HttpMessageConverter class from spring as:
public class CustomJacksonConverter implements HttpMessageConverter<Object>
{
public CustomJacksonConverter()
{
super();
//this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView(Views.ClientView.class));
this.delegate.getObjectMapper().configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
this.delegate.getObjectMapper().setSerializationInclusion(Include.NON_NULL);
}
// a real message converter that will respond to methods and do the actual work
private MappingJackson2HttpMessageConverter delegate = new MappingJackson2HttpMessageConverter();
#Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return delegate.canRead(clazz, mediaType);
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return delegate.canWrite(clazz, mediaType);
}
#Override
public List<MediaType> getSupportedMediaTypes() {
return delegate.getSupportedMediaTypes();
}
#Override
public Object read(Class<? extends Object> clazz,
HttpInputMessage inputMessage) throws IOException,
HttpMessageNotReadableException {
return delegate.read(clazz, inputMessage);
}
#Override
public void write(Object obj, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException
{
synchronized(this)
{
String userAgent = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getHeader("userAgent");
if ( userAgent != null )
{
switch (userAgent)
{
case "IOS" :
this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView(Views.IOS.class));
break;
case "Android" :
this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView(Views.Android.class));
break;
case "Web" :
this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView( Views.Web.class));
break;
default:
this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView( null ));
break;
}
}
else
{
// reset to default view
this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView( null ));
}
delegate.write(obj, contentType, outputMessage);
}
}
}
Now there is need to tell spring to use this custom json convert by simply putting this in dispatcher-servlet.xml
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean id="jsonConverter" class="com.mactores.org.CustomJacksonConverter" >
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
That's how you will able to decide which fields to get serialize.
Thanx
I have a generic interface with several implementation classes, which I need to serialise and deserialise via Json. I'm trying to get started with Jackson, using full data-binding, without much luck.
The sample code illustrates the problem:
import org.codehaus.jackson.map.*;
import org.codehaus.jackson.map.type.TypeFactory;
import org.codehaus.jackson.type.JavaType;
public class Test {
interface Result<T> {}
static class Success<T> implements Result<T> {
T value;
T getValue() {return value;}
Success(T value) {this.value = value;}
}
public static void main(String[] args) {
Result<String> result = new Success<String>("test");
JavaType type = TypeFactory.defaultInstance().constructParametricType(Result.class, String.class);
ObjectMapper mapper = new ObjectMapper().enableDefaultTyping();
ObjectWriter writer = mapper.writerWithType(type);
ObjectReader reader = mapper.reader(type);
try {
String json = writer.writeValueAsString(result);
Result<String> result2 = reader.readValue(json);
Success<String> success = (Success<String>)result2;
} catch (Throwable ex) {
System.out.print(ex);
}
}
}
The call to writeValueAsString to causes the following exception:
org.codehaus.jackson.map.JsonMappingException: No serializer found for class Test$Success and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS) )
Why is Jackson expecting me to register a serializer - I though the point of full data-binding was that I wouldn't need to do this?
Is the above approach correct?
First of all, you need to register the specialized type to use it with Jackson using the factory method TypeFactory.constructSpecializedType. Then, the specialized type should be a bean (it should have a default constructor, getters and setters) to deserialize it.
Take a look at these tests clarifiers.
#Test
public void canSerializeParametricInterface() throws IOException {
final ObjectMapper mapper = new ObjectMapper().enableDefaultTyping();
final JavaType baseInterface = TypeFactory.defaultInstance().constructParametricType(Result.class, String.class);
final JavaType subType = TypeFactory.defaultInstance().constructSpecializedType(baseInterface, Success.class);
final ObjectWriter writer = mapper.writerWithType(subType);
final String json = writer.writeValueAsString(Success.create("test"));
Assert.assertEquals("{\"value\":\"test\"}", json);
}
#Test
public void canDeserializeParametricInterface() throws IOException {
final ObjectMapper mapper = new ObjectMapper().enableDefaultTyping();
final JavaType baseInterface = TypeFactory.defaultInstance().constructParametricType(Result.class, String.class);
final JavaType subType = TypeFactory.defaultInstance().constructSpecializedType(baseInterface, Success.class);
final ObjectReader reader = mapper.reader(subType);
final Success<String> success = reader.readValue("{\"value\":\"test\"}");
Assert.assertEquals("test", success.getValue());
}
public static interface Result<T> {
}
public static class Success<T> implements Result<T> {
private T value;
public static <T> Success<T> create(T value) {
final Success<T> success = new Success<T>();
success.value = value;
return success;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
I have the provider interface
interface IProvider<T> {
T locate();
}
and a class containing a field of type IProvider (can be another type for other fields).
class MyObject {
MyLocator<String> field;
}
I need to serialize instances of MyObject to JSON using Jackson 1.7. The output must be the same as if MyObject.field had been a String (i.e. no reference to ILocator).
I can't figure out how to build the custom serializer required to achieve this. Here is the structure I am trying to use for this task:
class MyLocatorSerializer extends SerializerBase<MyLocator<?>> {
public MyLocatorSerializer() {
super(MyLocator.class, false);
}
#Override
public void serialize(MyLocator<?> a_value, JsonGenerator a_jgen,
SerializerProvider a_provider) throws IOException, JsonGenerationException {
// Insert code here to serialize a_value.locate(), whatever its type
}
#Override
public JsonNode getSchema(SerializerProvider a_provider, Type a_typeHint)
throws JsonMappingException {
// What should I return here? I can't find documentation regarding the different schema types...
}
}
The custom serializer would be registered using
SimpleModule module = new SimpleModule("MyModule", new Version(1, 0, 0, null));
module.addSerializer(new MyLocatorSerializer());
objectMapper.registerModule(module);
Another answer using mix-in annotations following the comment from Staxman.
static class JacksonCustomModule extends SimpleModule {
public JacksonCustomModule() {
super("JacksonCustomModule", new Version(1, 0, 0, null));
}
#Override
public void setupModule(SetupContext context) {
context.setMixInAnnotations(IProvider.class, IProviderMixIn.class);
super.setupModule(context);
}
interface IProviderMixIn<T> {
#JsonValue
T locate();
}
}
Activate the module with:
objectMapper.registerModule(new JacksonCustomModule());
Apologies if I misunderstand the question, but would this be as simple as just using #JsonValue on 'Locate' method, instead of writing a custom serializer?
What #JsonValue does is take value of a property as is, and use it instead of creating a JSON Object: often this is used for serializing a POJO as a simple String or number, like so:
public class StringWrapper {
#JsonValue public String value;
}
so that for class like:
public class POJO {
public StringWrapper wrapped;
}
we would get serialization like:
{
"wrapper" : "string value of 'value'"
}
instead of what would otherwise be seen:
{
"wrapper" : {
"value" : "... string value ... "
}
}
Annotation can be used for any types of values obviously.
Following StaxMan's answer, I inspected the workings of #JsonValue and got the following serializer:
// Based on JsonValueSerializer
private static class ProviderSerializer extends SerializerBase<IProvider<?>> {
public ProviderSerializer() {
super(IProvider.class, false);
}
#Override
public void serialize(IProvider<?> value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonGenerationException {
Object object = value.locate();
// and if we got null, can also just write it directly
if (object == null) {
provider.defaultSerializeNull(jgen);
return;
}
Class<?> c = object.getClass();
JsonSerializer<Object> ser = provider.findTypedValueSerializer(c, true, null);
// note: now we have bundled type serializer, so should NOT call with typed version
ser.serialize(object, jgen, provider);
}
#Override
public JsonNode getSchema(SerializerProvider provider, Type typeHint)
throws JsonMappingException {
// is this right??
return JsonSchema.getDefaultSchemaNode();
}
}
After some tests, this does what I need. However, I don't fully really understand the purpose of the getSchema method, so maybe I'm doing something wrong...
I am using the MappingJacksonJsonView in my SpringMVC application to render JSON from my controllers. I want the ObjectId from my object to render as .toString but instead it serializes the ObjectId into its parts. It works just fine in my Velocity/JSP pages:
Velocity:
$thing.id
Produces:
4f1d77bb3a13870ff0783c25
Json:
<script type="text/javascript">
$.ajax({
type: 'GET',
url: '/things/show/4f1d77bb3a13870ff0783c25',
dataType: 'json',
success : function(data) {
alert(data);
}
});
</script>
Produces:
thing: {id:{time:1327331259000, new:false, machine:974358287, timeSecond:1327331259, inc:-260555739},…}
id: {time:1327331259000, new:false, machine:974358287, timeSecond:1327331259, inc:-260555739}
inc: -260555739
machine: 974358287
new: false
time: 1327331259000
timeSecond: 1327331259
name: "Stack Overflow"
XML:
<script type="text/javascript">
$.ajax({
type: 'GET',
url: '/things/show/4f1d77bb3a13870ff0783c25',
dataType: 'xml',
success : function(data) {
alert(data);
}
});
</script>
Produces:
<com.place.model.Thing>
<id>
<__time>1327331259</__time>
<__machine>974358287</__machine>
<__inc>-260555739</__inc>
<__new>false</__new>
</id>
<name>Stack Overflow</name>
</com.place.model.Thing>
Is there a way to stop MappingJacksonJsonView from getting that much information out of the ObjectId? I just want the .toString() method, not all the details.
Thanks.
Adding the Spring config:
#Configuration
#EnableWebMvc
public class MyConfiguration {
#Bean(name = "viewResolver")
public ContentNegotiatingViewResolver viewResolver() {
ContentNegotiatingViewResolver contentNegotiatingViewResolver = new ContentNegotiatingViewResolver();
contentNegotiatingViewResolver.setOrder(1);
contentNegotiatingViewResolver.setFavorPathExtension(true);
contentNegotiatingViewResolver.setFavorParameter(true);
contentNegotiatingViewResolver.setIgnoreAcceptHeader(false);
Map<String, String> mediaTypes = new HashMap<String, String>();
mediaTypes.put("json", "application/x-json");
mediaTypes.put("json", "text/json");
mediaTypes.put("json", "text/x-json");
mediaTypes.put("json", "application/json");
mediaTypes.put("xml", "text/xml");
mediaTypes.put("xml", "application/xml");
contentNegotiatingViewResolver.setMediaTypes(mediaTypes);
List<View> defaultViews = new ArrayList<View>();
defaultViews.add(xmlView());
defaultViews.add(jsonView());
contentNegotiatingViewResolver.setDefaultViews(defaultViews);
return contentNegotiatingViewResolver;
}
#Bean(name = "xStreamMarshaller")
public XStreamMarshaller xStreamMarshaller() {
return new XStreamMarshaller();
}
#Bean(name = "xmlView")
public MarshallingView xmlView() {
MarshallingView marshallingView = new MarshallingView(xStreamMarshaller());
marshallingView.setContentType("application/xml");
return marshallingView;
}
#Bean(name = "jsonView")
public MappingJacksonJsonView jsonView() {
MappingJacksonJsonView mappingJacksonJsonView = new MappingJacksonJsonView();
mappingJacksonJsonView.setContentType("application/json");
return mappingJacksonJsonView;
}
}
And my controller:
#Controller
#RequestMapping(value = { "/things" })
public class ThingController {
#Autowired
private ThingRepository thingRepository;
#RequestMapping(value = { "/show/{thingId}" }, method = RequestMethod.GET)
public String show(#PathVariable ObjectId thingId, Model model) {
model.addAttribute("thing", thingRepository.findOne(thingId));
return "things/show";
}
}
By default Jackson provides the serialization of Object received. ObjectId returns the Object therefor its attributes are visible after conversion to JSON. You need to specify the type of serialization required, Here in this case it is string. Thing entity class which is used to create ThingRepository will look like this to get this done:
public class Thing {
#Id
#JsonSerialize(using= ToStringSerializer.class)
ObjectId id;
String name;
}
Here make a note of added anotation #JsonSerialize(using= ToStringSerializer.class) which instructs to serialize the ObjectID to String.
Previous answer did the trick, but it was ugly and not well thought out - a clear workaround to actually fixing the problem.
The real issue is that ObjectId deserializes into its component parts. MappingJacksonJsonView sees ObjectId for what it is, an object, and goes to work on it. The deserialized fields being seen in the JSON are the fields that make up an ObjectId. To stop the serialization/deserialization of such an object, you have to configure a CustomObjectMapper that extends ObjectMapper.
Here is the CustomeObjectMapper:
public class CustomObjectMapper extends ObjectMapper {
public CustomObjectMapper() {
CustomSerializerFactory sf = new CustomSerializerFactory();
sf.addSpecificMapping(ObjectId.class, new ObjectIdSerializer());
this.setSerializerFactory(sf);
}
}
Here is the ObjectIdSerializer that the CustomObjectMapper uses:
public class ObjectIdSerializer extends SerializerBase<ObjectId> {
protected ObjectIdSerializer(Class<ObjectId> t) {
super(t);
}
public ObjectIdSerializer() {
this(ObjectId.class);
}
#Override
public void serialize(ObjectId value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
jgen.writeString(value.toString());
}
}
And here is what needs to change in your #Configuration-annotated class:
#Bean(name = "jsonView")
public MappingJacksonJsonView jsonView() {
final MappingJacksonJsonView mappingJacksonJsonView = new MappingJacksonJsonView();
mappingJacksonJsonView.setContentType("application/json");
mappingJacksonJsonView.setObjectMapper(new CustomObjectMapper());
return mappingJacksonJsonView;
}
You are basically telling Jackson how to serialize/deserialize this particular object. Works like a charm.
If you're using autowired instance of the auto configured mapper in Spring Boot, you can just add this customizer bean:
#Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
return builder -> builder.serializerByType(ObjectId.class, ToStringSerializer.instance);
}
Relevant imports:
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.bson.types.ObjectId;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
And then this will reflect anywhere the autowired mapper is used, for example:
#Service
public class MyService {
private final ObjectMapper objectMapper;
private final MongoTemplate mongoTemplate;
#Autowired
public MyService(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public String getJsonForMongoCommand(Document document) {
return objectMapper.writeValueAsString(mongoTemplate.executeCommand(document));
}
}
Or in this specific case (untested, might be unnecessary):
#Bean(name = "jsonView")
public MappingJacksonJsonView jsonView(ObjectMapper objectMapper) {
final MappingJacksonJsonView mappingJacksonJsonView = new MappingJacksonJsonView();
mappingJacksonJsonView.setContentType("application/json");
mappingJacksonJsonView.setObjectMapper(objectMapper);
return mappingJacksonJsonView;
}
I had to just make the getId() method return a String. It was the only way to make Jackson stop serializing the ObjectId.
public String getId() {
if (id != null) {
return id.toString();
} else {
return null;
}
}
public void setId(ObjectId id) {
this.id = id;
}
setId() still has to be ObjectId so Mongo (and its driver) can set the ID correctly.
Just to complement the answers, if you face a scenario where you also need to serialize an array of ObjectId, you could create a custom serializer with the following logic:
public class ObjectIdSerializer extends JsonSerializer<Object> {
#Override
public void serialize(final Object value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException {
if (value instanceof Collection) {
final Collection<String> ids = new ArrayList<>();
for (final Object id : ((Collection<?>) value)) {
ids.add(ObjectId.class.cast(id).toString());
}
jgen.writeObject(ids);
} else {
jgen.writeString(ObjectId.class.cast(value).toString());
}
}
}