KafkaStreams serde exception - java

i am playing with Kafka and streams technology; i have created a custom serializer and deserializer for the KStream that i will use to receive messages from a given topic.
Now, the problem is that i am creating a serde in this way:
JsonSerializer<EventMessage> serializer = new JsonSerializer<>();
JsonDeserializer<EventMessage> deserializer = new JsonDeserializer<>(EventMessage.class);
Serde<EventMessage> messageSerde = Serdes.serdeFrom(serializer, deserializer);
Serializer implementation:
public class JsonSerializer<T> implements Serializer<T> {
private Gson gson = new Gson();
public void configure(Map<String, ?> map, boolean b) {
}
#Override
public byte[] serialize(String topic, T data) {
return gson.toJson(data).getBytes(Charset.forName("UTF-8"));
}
#Override
public void close() {
}
}
Deserializer implementation:
public class JsonDeserializer<T> implements Deserializer<T> {
private Gson gson = new Gson();
private Class<T> deserializedClass;
public JsonDeserializer() {
}
public JsonDeserializer(Class<T> deserializedClass) {
this.deserializedClass = deserializedClass;
}
#Override
#SuppressWarnings("unchecked")
public void configure(Map<String, ?> map, boolean b) {
if(deserializedClass == null) {
deserializedClass = (Class<T>) map.get("serializedClass");
}
}
#Override
public T deserialize(String topic, byte[] data) {
System.out.print(data);
if(data == null){
return null;
}
return gson.fromJson(new String(data),deserializedClass);
}
#Override
public void close() {
}
}
When i try to execute the code, i receive the following error:
Caused by: org.apache.kafka.common.KafkaException: Could not instantiate class org.apache.kafka.common.serialization.Serdes$WrapperSerde Does it have a public no-argument constructor?
Full dump here: https://pastebin.com/WwpuXuxB
This is the way i am trying to use serde:
KStreamBuilder builder = new KStreamBuilder();
KStream<String, EventMessage> eventsStream = builder.stream(stringSerde, messageSerde, topic);
KStream<String, EventMessage> outStream = eventsStream
.mapValues(value -> EventMessage.build(value.type, value.timestamp));
outStream.to("output");
Also, i am not totally sure i am setting up correctly the properties to setup up serializer and deserializer globally:
streamsConfiguration.put(StreamsConfig.KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
streamsConfiguration.put(StreamsConfig.VALUE_SERDE_CLASS_CONFIG, messageSerde.getClass());

To complete the Matthias answer I've just coded a simple example of how to create a custom Serde (Serializer / Deserializer) within a Kafka Stream App. It's is available to clone and try in: https://github.com/Davidcorral94/Kafka-Streams-Custom-Seder
First I create two classes, one for the Serializer and another for the Deserializer. In this case I use Gson library to perform the serialization/deserialization.
Serializer
public class PersonSerializer implements Closeable, AutoCloseable, Serializer<Person> {
private static final Charset CHARSET = Charset.forName("UTF-8");
static private Gson gson = new Gson();
#Override
public void configure(Map<String, ?> map, boolean b) {
}
#Override
public byte[] serialize(String s, Person person) {
// Transform the Person object to String
String line = gson.toJson(person);
// Return the bytes from the String 'line'
return line.getBytes(CHARSET);
}
#Override
public void close() {
}
}
Deserializer
public class PersonDeserializer implements Closeable, AutoCloseable, Deserializer<Person> {
private static final Charset CHARSET = Charset.forName("UTF-8");
static private Gson gson = new Gson();
#Override
public void configure(Map<String, ?> map, boolean b) {
}
#Override
public Person deserialize(String topic, byte[] bytes) {
try {
// Transform the bytes to String
String person = new String(bytes, CHARSET);
// Return the Person object created from the String 'person'
return gson.fromJson(person, Person.class);
} catch (Exception e) {
throw new IllegalArgumentException("Error reading bytes", e);
}
}
#Override
public void close() {
}
}
Then, I wrap both of them into a Serde to be able to use it into my Kafka Stream App.
Serde
public class PersonSerde implements Serde<Person> {
private PersonSerializer serializer = new PersonSerializer();
private PersonDeserializer deserializer = new PersonDeserializer();
#Override
public void configure(Map<String, ?> configs, boolean isKey) {
serializer.configure(configs, isKey);
deserializer.configure(configs, isKey);
}
#Override
public void close() {
serializer.close();
deserializer.close();
}
#Override
public Serializer<Person> serializer() {
return serializer;
}
#Override
public Deserializer<Person> deserializer() {
return deserializer;
}
}
Finally, you are able to use this Serde class into your Kafka Stream App with the next line:
props.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, PersonSerde.class);
This is actually working with the latest Kafka version available at this moment which is 1.0.0!

If you call Serdes.serdeFrom(...) you get a WrappedSerde type back that is for internal usage (and WrappedSerde does not have an non-argument constructor). There is currently no API you can call to get a custom Serde. Instead, you need to implement you own Serde class and wrap you serializer and deserializer "manually".
public class EventMessageSerde implements Serde<EventMessage> {
final private JsonSerializer<EventMessage> serializer;
final private JsonDeserializer<EventMessage> deserializer;
#Override
public void configure(Map<String, ?> configs, boolean isKey) {
serializer.configure(configs, isKey);
deserializer.configure(configs, isKey);
}
#Override
public void close() {
serializer.close();
deserializer.close();
}
#Override
public Serializer<EventMessage> serializer() {
return serializer;
}
#Override
public Deserializer<EventMessage> deserializer() {
return deserializer;
}
}
In your Properties you can set:
streamsConfiguration.put(StreamsConfig.VALUE_SERDE_CLASS_CONFIG, EventMessageSerde.class);

Another way is using StreamsBuilder instead of KStreamBuilder. KStreamBuilder is deprecated in 1.0.0. You can directly pass serde object using Consumed.with while creating stream. You need not to create custom Serde class in this scenario.
Serde<EventMessage> messageSerde = Serdes.serdeFrom(serializer, deserializer);
StreamsBuilder builder = new StreamsBuilder();
KStream<String, EventMessage> eventsStream = builder.stream(topic, Consumed.with(Serdes.String(), messageSerde));
You can keep StringSerde in below code instead of using messageSerde.getClass() which is failing because messageSerde is just a WrappedSerde that does not have non-argument constructor.
streamsConfiguration.put(StreamsConfig.VALUE_SERDE_CLASS_CONFIG, StringSerde.class.getName());

Related

Spring filtering response fields with jackson

I am trying to filter fields in a nested object:
class Response {
// These objects themselves can have many fields within
private final PropA a;
private final PropB b;
#JsonCreator
public Response(PropA a, PropB b) { ... }
}
I'd like a generic 'filter helper' to achieve the above logic. Here is what I have so far (following a similar approach as this project)
public class FilterHelper {
private final ObjectMapper objectMapper;
public FilterHelper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.objectMapper.addMixIn(Object.class, MyFilterMixin.class);
}
#JsonFilter("myfilter")
public static class MyFilterMixin {
}
private static class MyFilter extends SimpleBeanPropertyFilter {
private final Set<String> properties;
public MyFilter(Set<String> properties) {
super();
this.properties = properties;
}
#Override
public void serializeAsField(final Object pojo, final JsonGenerator jgen, final SerializerProvider provider,
final PropertyWriter writer) throws Exception {
System.out.println("************************** " + writer.getName());
if (properties.contains(writer.getName())) {
writer.serializeAsField(pojo, jgen, provider);
} else if (!jgen.canOmitFields()) {
writer.serializeAsOmittedField(pojo, jgen, provider);
}
}
}
public String filter(T obj, Set<String> fields) {
FilterProvider filterProvider = new SimpleFilterProvider().addFilter("myfilter", new MyFilter(fields));
return objectMapper.writer(filterProvider).writeValueAsString(obj);
}
}
When I hit this endpoint with ?fields=one,two as query parameter I expect to see from a line printed to console for every field within that top level Response object as follows:
******************* a
******************* a1
******************* a2
******************* ..etc
******************* b
******************* b1
******************* b2
******************* ..etc
but I am only seeing output for the top level a and b fields followed by an error before getting a 500 status code from the endpoint:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot resolve PropertyFilter with id 'myfilter'; no FilterProvider configured (through reference chain: com.google.common.collect.SingletonImmutableList[0])
It is worth mentioning that I had this working somehow, but it was broken after some changes I don't recall.
Unless you need to provide custom serialization for different fields, you should not be hooking the serializeAsField and instead you should be overriding the #include variant methods:
com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter#include(com.fasterxml.jackson.databind.ser.BeanPropertyWriter)
com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter#include(com.fasterxml.jackson.databind.ser.PropertyWriter)
as follows:
private static class MyFilter extends SimpleBeanPropertyFilter {
private final Set<String> properties;
public MyFilter(Set<String> properties) {
super();
this.properties = properties;
}
#Override
protected boolean include(BeanPropertyWriter writer) {
return !this.properties.contains(writer.getName());
}
#Override
protected boolean include(PropertyWriter writer) {
return !this.properties.contains(writer.getName());
}
}
There is even a static factory providing a com.fasterxml.jackson.databind.ser.PropertyFilter that filters out a specific set of fields:
com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter#serializeAllExcept(java.util.Set<java.lang.String>)
Extra issue
At the filter helper level, you are serializing the filtered object to JSON then deserializing it back (with filtered fields) to an object that you are handing back as the endpoint response.
Solution / Alternative
You can simply omit the intermediary step by just sterilizing the result Response with the filter fields predicate and returning the result JSON as ResponseEntity:
FilterHelper:
#Component
public class FilterHelper {
private final ObjectMapper objectMapper;
#Autowired
public FilterHelper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.objectMapper.addMixIn(Object.class, MyFilterMixin.class);
}
#JsonFilter("myfilter")
public static class MyFilterMixin {
}
private static class MyFilter extends SimpleBeanPropertyFilter {
private final Set<String> properties;
public MyFilter(Set<String> properties) {
super();
this.properties = properties;
}
#Override
protected boolean include(BeanPropertyWriter writer) {
return !this.properties.contains(writer.getName());
}
#Override
protected boolean include(PropertyWriter writer) {
return !this.properties.contains(writer.getName());
}
}
public String filter(Object dto, Set<String> fields) {
if (fields == null || fields.isEmpty()) {
return "";
}
FilterProvider filterProvider = new SimpleFilterProvider()
.addFilter("myfilter", SimpleBeanPropertyFilter.serializeAllExcept(fields));
try {
return objectMapper.writer(filterProvider).writeValueAsString(dto);
} catch (JsonProcessingException e) {
return "";
}
}
}
Controller:
#GetMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseStatus(OK)
ReponseEntity<String> someEndpoint(#RequestParam(name = "fields") Set<String> fields) {
Response response = getResponseFromSomewhere();
return ResponseEntity.ok(filterHelper.filter(response, fields));
}

Serialize class with generic type using gson?

I have the following class
private static class ClassWithGenericType<T> {
Set<T> values;
}
If I initialize now the class with a Set of Enum-values, serialize and deserialize the object by using gson, the Set of the deserialized object does not contain the Enum-values, but the values as String.
I think this is because the generic type is thrown away through the serialization. I saw, that I could use new TypeToken<...>(){}.getType();, but the problem is, that the class above is part of a bigger object, so I cannot call gson.fromJson(classWithGenericType, typeToken) directly.
Is there a smart way of solving this problem? I thought of a TypeAdapter, which does not serialize only the values of the Set, but also it's type.
I found now a solution and created a TypeAdapter.
public class SetTypeAdapterFactory implements TypeAdapterFactory {
#Override
public <T> TypeAdapter<T> create(Gson gson, #NonNull TypeToken<T> type) {
if (!Set.class.isAssignableFrom(type.getRawType())) {
return null;
}
return (TypeAdapter<T>) new SetTypeAdapter(gson);
}
}
public class SetTypeAdapter extends TypeAdapter<Set<?>> {
public static final String TYPE = "#type";
public static final String DATA = "#data";
private final Gson gson;
public SetTypeAdapter(#NonNull Gson gson) {
this.gson = gson;
}
#Override
public void write(final JsonWriter out, final Set<?> set
) throws IOException {
out.beginArray();
for (Object item : set) {
out.beginObject();
out.name(TYPE).value(item.getClass().getName());
out.name(DATA).jsonValue(gson.toJson(item));
out.endObject();
}
out.endArray();
}
#Override
public Set<?> read(final JsonReader in) throws IOException {
final Set<Object> set = Sets.newHashSet();
in.beginArray();
while (in.hasNext()) {
in.beginObject();
set.add(readNextObject(in));
in.endObject();
}
in.endArray();
return set;
}
private Object readNextObject(JsonReader in) throws IOException {
try {
checkNextName(in, TYPE);
Class<?> cls = Class.forName(in.nextString());
checkNextName(in, DATA);
return gson.fromJson(in, cls);
} catch (ClassNotFoundException exception) {
throw new IOException(exception);
}
}
private void checkNextName(JsonReader in, String name) throws IOException {
if (!in.nextName().equals(name)) {
throw new IOException("Name was not: " + name);
}
}
}
We can add the factory to the GsonBuilder and afterwards we are capable of serializing a Set with generic types.
var gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapterFactory(new SetTypeAdapterFactory());
var gson = gsonBuilder.create();
The serialized Set has then the following structure:
[
{
"#type":<class_name_first_element>,
"#data":<first_element_as_json>
},
...
]

Jackson - combine #JsonValue and #JsonSerialize

I am trying a combination of #JsonValue and #JsonSerialize. Let's start with my current container class:
public class Container {
private final Map<SomeKey, Object> data;
#JsonValue
#JsonSerialize(keyUsing = SomeKeySerializer.class)
public Map<SomeKey, Object> data() {
return data;
}
}
In this case, the custom serializer SomeKeySerializer is not used.
If I change the container as following, the serializer is called:
public class Container {
#JsonSerialize(keyUsing = SomeKeySerializer.class)
private final Map<SomeKey, Object> data;
}
However, this is not what I want, as this introduces another 'data' level in the output JSON.
Is it possible to combine #JsonValue and #JsonSerialize in some way?
I could always write another custom serializer for Container, which more or less does the same as the functionality behind #JsonValue. This would be more or less a hack, in my opinion.
Jackson version: 2.6.2
This combination seems to do what you want: make a Converter to extract the Map from the Container, and add #JsonValue to SomeKey itself to serialize it:
#JsonSerialize(converter = ContainerToMap.class)
public class ContainerWithFieldData {
private final Map<SomeKey, Object> data;
public ContainerWithFieldData(Map<SomeKey, Object> data) {
this.data = data;
}
}
public static final class SomeKey {
public final String key;
public SomeKey(String key) {
this.key = key;
}
#JsonValue
public String toJsonValue() {
return "key:" + key;
}
#Override
public String toString() {
return "SomeKey:" + key;
}
}
public static final class ContainerToMap extends StdConverter<ContainerWithFieldData, Map<SomeKey, Object>> {
#Override
public Map<SomeKey, Object> convert(ContainerWithFieldData value) {
return value.data;
}
}
#Test
public void serialize_container_with_custom_keys_in_field_map() throws Exception {
ObjectMapper mapper = new ObjectMapper();
assertThat(
mapper.writeValueAsString(new ContainerWithFieldData(ImmutableMap.of(new SomeKey("key1"), "value1"))),
equivalentTo("{ 'key:key1' : 'value1' }"));
}
I simply can't get annotating an accessor method of Container to DTRT at all easily, not in combination with #JsonValue. Given that #JsonValue on the container is basically designating a converter anyway (that is implemented by calling the annotated method), this is effectively what you're after, although not as pleasant as it seems it should be. (tried with Jackson 2.6.2)
(Something I learned from this: key serializers aren't like normal serializers, even though they implement JsonSerializer just the same. They need to call writeFieldName on the JsonGenerator, not writeString, for example. On the deserialization side, the distinction between JsonDeserializer and KeyDeserializer is spelled out, but not on the serialization side. You can make a key serializer from SomeKey with #JsonValue, but not by annotating SomeKey with #JsonSerialize(using=...), which surprised me).
Have you tried using #JsonSerialize(using = SomeKeySerializer.class) instead of keyUsing?
Doc for using() says:
Serializer class to use for serializing associated value.
...while for keyUsing you get:
Serializer class to use for serializing Map keys of annotated property
Tested it out myself and it works...
public class Demo {
public static class Container {
private final Map<String, String> data = new HashMap<>();
#JsonValue
#JsonSerialize(using = SomeKeySerializer.class)
public Map<String, String> data() {
return data;
}
}
public static class SomeKeySerializer extends JsonSerializer<Map> {
#Override
public void serialize(Map value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeObjectField("aKeyInTheMap", "theValueForThatKey");
jgen.writeEndObject();
}
}
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
String s = objectMapper.writeValueAsString(new Container());
System.out.println(s);
}
}
This is the output when I'm NOT using com.fasterxml.jackson.annotation.JsonValue
{
"data" : {
"aKeyInTheMap" : "theValueForThatKey"
}
}
And this is the output when I'm using com.fasterxml.jackson.annotation.JsonValue
{
"aKeyInTheMap" : "theValueForThatKey"
}

Serialising generic interface sub-class with Jackson

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;
}
}

Json deserialization into other class hierarchy using Jackson

Now i'm working with Jackson and i have some questions about it.
First of all. I have two services, first is data collecting and sending service and second receive this data and, for example, log it into a file.
So, first service has class hierarchy like this:
+----ConcreteC
|
Base ----+----ConcreteA
|
+----ConcreteB
And second service has class hierarchy like this:
ConcreteAAdapter extends ConcreteA implements Adapter {}
ConcreteBAdapter extends ConcreteB implements Adapter {}
ConcreteCAdapter extends ConcreteC implements Adapter {}
The first service knows nothing about ConcreteXAdapter.
The way i'm sending the data on the first service:
Collection<Base> data = new LinkedBlockingQueue<Base>()
JacksonUtils utils = new JacksonUtils();
data.add(new ConcreteA());
data.add(new ConcreteB());
data.add(new ConcreteC());
...
send(utils.marshall(data));
...
public class JacksonUtils {
public byte[] marshall(Collection<Base> data) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream() {
#Override
public byte[] toByteArray() {
return buf;
}
};
getObjectMapper().writeValue(out, data);
return out.toByteArray();
}
protected ObjectMapper getObjectMapper() {
return new ObjectMapper();
}
public Object unmarshall(byte[] json) throws IOException {
return getObjectMapper().readValue(json, Object.class);
}
public <T> T unmarshall(InputStream source, TypeReference<T> typeReference) throws IOException {
return getObjectMapper().readValue(source, typeReference);
}
public <T> T unmarshall(byte[] json, TypeReference<T> typeReference) throws IOException {
return getObjectMapper().readValue(json, typeReference);
}
}
So, i want to desirialize json into Collection of ConcreteXAdapter, not into Collection of ConcreteX (ConcreteA -> ConcreteAAdapter, ConcreteB -> ConcreteBAdapter, ConcreteC -> ConcreteCAdapter). In the case i described i want to get:
Collection [ConcreteAAdapter, ConcreteBAdapter, ConcreteCAdapter]
How can i do this?
For this purpose you need to pass additional info in JSON:
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME,
include=JsonTypeInfo.As.PROPERTY, property="#type")
class Base {
...
}
Then on serialization it will add #type field:
objectMapper.registerSubtypes(
new NamedType(ConcreteAAdapter.class, "ConcreteA"),
new NamedType(ConcreteBAdapter.class, "ConcreteB"),
new NamedType(ConcreteCAdapter.class, "ConcreteC")
);
// note, that for lists you need to pass TypeReference explicitly
objectMapper.writerWithType(new TypeReference<List<Base>>() {})
.writeValueAsString(someList);
{
"#type" : "ConcreteA",
...
}
on deserialization it will be:
objectMapper.registerSubtypes(
new NamedType(ConcreteA.class, "ConcreteA"),
new NamedType(ConcreteB.class, "ConcreteB"),
new NamedType(ConcreteC.class, "ConcreteC")
);
objectMapper.readValue(....)
More info here
How I solved this problem. Here is a class diagram for an example project:
So i want to get the ConcreteAAdapter form ConcreteA after deserialization.
My solution is to extend ClassNameIdResolver to add functionality to deserialize base class objects into subtype class objects (subtype classes adds no extra functionality and additional fields).
Here is a code which creates ObjectMapper for deserialization:
protected ObjectMapper getObjectMapperForDeserialization() {
ObjectMapper mapper = new ObjectMapper();
StdTypeResolverBuilder typeResolverBuilder = new ObjectMapper.DefaultTypeResolverBuilder(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
typeResolverBuilder = typeResolverBuilder.inclusion(JsonTypeInfo.As.PROPERTY);
typeResolverBuilder.init(JsonTypeInfo.Id.CLASS, new ClassNameIdResolver(SimpleType.construct(Base.class), TypeFactory.defaultInstance()) {
private HashMap<Class, Class> classes = new HashMap<Class, Class>() {
{
put(ConcreteA.class, ConcreteAAdapter.class);
put(ConcreteB.class, ConcreteBAdapter.class);
put(ConcreteC.class, ConcreteCAdapter.class);
}
};
#Override
public String idFromValue(Object value) {
return (classes.containsKey(value.getClass())) ? value.getClass().getName() : null;
}
#Override
public JavaType typeFromId(String id) {
try {
return classes.get(Class.forName(id)) == null ? super.typeFromId(id) : _typeFactory.constructSpecializedType(_baseType, classes.get(Class.forName(id)));
} catch (ClassNotFoundException e) {
// todo catch the e
}
return super.typeFromId(id);
}
});
mapper.setDefaultTyping(typeResolverBuilder);
return mapper;
}
And here is a code which create ObjectMapper for serialization:
protected ObjectMapper getObjectMapperForSerialization() {
ObjectMapper mapper = new ObjectMapper();
StdTypeResolverBuilder typeResolverBuilder = new ObjectMapper.DefaultTypeResolverBuilder(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
typeResolverBuilder = typeResolverBuilder.inclusion(JsonTypeInfo.As.PROPERTY);
typeResolverBuilder.init(JsonTypeInfo.Id.CLASS, new ClassNameIdResolver(SimpleType.construct(Base.class), TypeFactory.defaultInstance()));
mapper.setDefaultTyping(typeResolverBuilder);
return mapper;
}
Test code:
public static void main(String[] args) throws IOException {
JacksonUtils JacksonUtils = new JacksonUtilsImpl();
Collection<Base> data = new LinkedBlockingQueue<Base>();
data.add(new ConcreteA());
data.add(new ConcreteB());
data.add(new ConcreteC());
String json = JacksonUtils.marshallIntoString(data);
System.out.println(json);
Collection<? extends Adapter> adapters = JacksonUtils.unmarshall(json, new TypeReference<ArrayList<Adapter>>() {});
for (Adapter adapter : adapters) {
System.out.println(adapter.getClass().getName());
}
}
Full code of JacksonUtils class:
public class JacksonUtilsImpl implements JacksonUtils {
#Override
public byte[] marshall(Collection<Base> data) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream() {
#Override
public byte[] toByteArray() {
return buf;
}
};
getObjectMapperForSerialization().writerWithType(new TypeReference<Collection<Base>>() {}).writeValue(out, data);
return out.toByteArray();
}
#Override
public String marshallIntoString(Collection<Base> data) throws IOException {
return getObjectMapperForSerialization().writeValueAsString(data);
}
protected ObjectMapper getObjectMapperForSerialization() {
ObjectMapper mapper = new ObjectMapper();
StdTypeResolverBuilder typeResolverBuilder = new ObjectMapper.DefaultTypeResolverBuilder(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
typeResolverBuilder = typeResolverBuilder.inclusion(JsonTypeInfo.As.PROPERTY);
typeResolverBuilder.init(JsonTypeInfo.Id.CLASS, new ClassNameIdResolver(SimpleType.construct(Base.class), TypeFactory.defaultInstance()));
mapper.setDefaultTyping(typeResolverBuilder);
return mapper;
}
protected ObjectMapper getObjectMapperForDeserialization() {
ObjectMapper mapper = new ObjectMapper();
StdTypeResolverBuilder typeResolverBuilder = new ObjectMapper.DefaultTypeResolverBuilder(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE);
typeResolverBuilder = typeResolverBuilder.inclusion(JsonTypeInfo.As.PROPERTY);
typeResolverBuilder.init(JsonTypeInfo.Id.CLASS, new ClassNameIdResolver(SimpleType.construct(Base.class), TypeFactory.defaultInstance()) {
private HashMap<Class, Class> classes = new HashMap<Class, Class>() {
{
put(ConcreteA.class, ConcreteAAdapter.class);
put(ConcreteB.class, ConcreteBAdapter.class);
put(ConcreteC.class, ConcreteCAdapter.class);
}
};
#Override
public String idFromValue(Object value) {
return (classes.containsKey(value.getClass())) ? value.getClass().getName() : null;
}
#Override
public JavaType typeFromId(String id) {
try {
return classes.get(Class.forName(id)) == null ? super.typeFromId(id) : _typeFactory.constructSpecializedType(_baseType, classes.get(Class.forName(id)));
} catch (ClassNotFoundException e) {
// todo catch the e
}
return super.typeFromId(id);
}
});
mapper.setDefaultTyping(typeResolverBuilder);
return mapper;
}
#Override
public Object unmarshall(byte[] json) throws IOException {
return getObjectMapperForDeserialization().readValue(json, Object.class);
}
#Override
public <T> T unmarshall(InputStream source, TypeReference<T> typeReference) throws IOException {
return getObjectMapperForDeserialization().readValue(source, typeReference);
}
#Override
public <T> T unmarshall(byte[] json, TypeReference<T> typeReference) throws IOException {
return getObjectMapperForDeserialization().readValue(json, typeReference);
}
#Override
public <T> Collection<? extends T> unmarshall(String json, Class<? extends Collection<? extends T>> klass) throws IOException {
return getObjectMapperForDeserialization().readValue(json, klass);
}
#Override
public <T> Collection<? extends T> unmarshall(String json, TypeReference typeReference) throws IOException {
return getObjectMapperForDeserialization().readValue(json, typeReference);
}
}
I find programmerbruce's approach to be the most clear and easy to get working (example below).
I got the information from his answer to a related question:
https://stackoverflow.com/a/6339600/1148030
and the related blog post:
http://programmerbruce.blogspot.fi/2011/05/deserialize-json-with-jackson-into.html
Also check out this friendly wiki page (also mentioned in Eugene Retunsky's answer):
https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization
Another nice wiki page: https://github.com/FasterXML/jackson-docs/wiki/JacksonMixInAnnotations
Here is a short example to give you the idea:
Configure the ObjectMapper like this:
mapper.getDeserializationConfig().addMixInAnnotations(Base.class, BaseMixin.class);
mapper.getSerializationConfig().addMixInAnnotations(Base.class, BaseMixin.class);
Example BaseMixin class (easy to define as an inner class.)
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="type")
#JsonSubTypes({
#JsonSubTypes.Type(value=ConcreteA.class, name="ConcreteA"),
#JsonSubTypes.Type(value=ConcreteB.class, name="ConcreteB")
})
private static class BaseMixin {
}
On second service you could define the BaseMixin like this:
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="type")
#JsonSubTypes({
#JsonSubTypes.Type(value=ConcreteAAdapter.class, name="ConcreteA"),
#JsonSubTypes.Type(value=ConcreteBAdapter.class, name="ConcreteB")
})
private static class BaseMixin {
}

Categories