How to implements polymorphic serialization with out annotation and mixin - java

In jackson, we can uses the annotations
#JsonTypeInfo
#JsonSubTypes
#JsonSubTypes.Type
to implement polymorphic serialization.
We can choose to
Use these annotations on data model directly, this is the simplest way.
Use these annotations on mixin. Here is a link about it Polymorphic deserialization in Jackson without annotations.
Both of these two solutions have a problem: All the sub classes must be known when writing code.
In GraphQL
The discriminator field is fixed: "__typename"
The sub type names are fixed too: Simple name of java classes
All the requirements are fixed, that means it unnecessary to configure sub types one by one, it's possible to create a jackson module to handle them automatically.
// An empty interface
// Developers need not to configure polymorphic metadata for any class of its subtypes
public interface GraphQLObject {}
public class BookStore implements GraphQLObject {
public List<Book> getBooks() {...}
...other gettes/setters...
}
public abstract class Book implements GraphQLObject {
... some properties ...
}
public class ElectronicBook extends Book {
... some properties ...
}
public class PaperBook extends Book {
... some properties ...
}
The usage code looks like this
BookStore store = ...;
ObjectMapper mapper = new ObjectMapper();
mapper.addModule(new GraphQLModule());
System.out.println(mapper.writeValueAsString(store));
Here, we need to create "GraphQLModule", it can handle all the sub types implement the empty interface "GraphQLObject", and tell jackson how to use the simple class name of each subtype to be the value of discriminator field "__typename"
The result should looks like:
{
name: "store",
books: [
{ __typename: "ElectronicBook", name: "book-1" },
{ __typename: "PaperBook", name: "book-2" }
]
}
Is it possible to implement the "GraphQLModule"?
Note:
Like the default polymorphic behavior of jackson, discriminator field only need to be added when the object runtime type is different with the generic type argument of list which is known when compile.

I found the reason.
I try to defined customer serializer, but I found "serializeWithType" is never called.
In my project, data type is interface. I use ASM to generate its bytecode. I only generated the simplest bytecode and ignored the signature for generic.
So, in the inteface, it's List<Book>
But, in my bytecode implementation, it's List

It is possible to implement the "GraphQLModule" module extending the SimpleModule class:
public class GraphQLModule extends SimpleModule {
public GraphQLModule() {
this.addSerializer(new GraphQLSerializer());
}
}
I added inside the module a new serializer that extends the StdSerializer class:
public class GraphQLSerializer extends StdSerializer<GraphQLObject> {
public GraphQLSerializer() {
super(GraphQLObject.class);
}
#Override
public void serialize(GraphQLObject obj, JsonGenerator jg, SerializerProvider sp) throws IOException {
jg.writeStartObject();
jg.writeStringField("__typename", obj.getClass().getSimpleName());
jg.writeEndObject();
}
}
The GraphQLSerializer serializer simply takes your object implementing your GraphQLObject interface and serialize it including in the json just the classname string of the object as a __typename.
So you can add register this module to your objectMapper and use it like in this example :
public interface GraphQLObject {}
public abstract class Book implements GraphQLObject {}
public class ElectronicBook extends Book {}
public class PaperBook extends Book {}
ObjectMapper objectMapper = new ObjectMapper();
mapper.registerModule(new GraphQLModule());
List<Book> books = List.of(new ElectronicBook(), new PaperBook());
//it will print [{"__typename":"ElectronicBook"},{"__typename":"PaperBook"}]
System.out.println(mapper.writeValueAsString(books));

Related

Deserialize interface to specific type with jackson without JsonSubTypes

I have a project A where I have an interface like this:
interface MyInterface extends Serializable { }
In another project B I have a class that implements that interface:
#Data
class MyClass implements MyInterface {
private String someProp;
}
Now I would like to tell jackson that I want to deserialize all appearances of MyInterface as MyClass. I know it is normally possible to use JsonSubTypes but in this case Project A does not know Project B.
Is there maybe a way to get the default deserializer for a type? Then I could just do something like this:
SimpleModule module = new SimpleModule();
module.addDeserializer(MyInterface.class, DefaultDeserializerForMyClass);
I know that I could write custom deserializers that do exactly the same, but is there an easier way?
you could add #JsonDeserialize on MyClass and use ObjectMapper.addMixIn() with MyInterface as target.
public ObjectMapper addMixIn(Class target,
Class mixinSource)
Method to use for adding mix-in annotations to use for augmenting
specified class or interface. All annotations from mixinSource are
taken to override annotations that target (or its supertypes) has.
target - Class (or interface) whose annotations to effectively override
mixinSource - Class (or interface) whose annotations are to be "added" to target's annotations, overriding as necessary
for example:
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
#JsonDeserialize(as = MyClass.class)
class MyClass implements MyInterface
{
private String someProp;
/* getters and setters */
}
#Bean
public ObjectMapper objectMapper()
{
ObjectMapper om = new ObjectMapper();
om.addMixIn(MyInterface.class, MyClass.class);
return om;
}
#PostMapping
public String foo(#RequestBody MyInterface bar)
{
if (bar instanceof MyClass) {
MyClass baz = (MyClass)bar;
System.out.println(baz.getSomeProp());
return "world"
}
return "goodbye"
}
$ curl -X POST -d '{"someProp": "hello"}' -H "content-type: application/json" localhost:8080
world
and server correctly prints:
hello

Use JsonIgnore for an attribute in an other class

I meet an issue with a class contained in a library that I use.
This issue comes when I want deserialize it.
Indeed, this class has a method names "getCopy" which returns a new instance of himself which contains this same method and call it still a StackOverFlowException on the following cycle :
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:166)
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:723)
public class Object {
...
ObjectAttribute objectAttribute;
...
public ObjectAttribute getObjectAttribute(){
return this.objectAttribute
}
...
}
public class ObjectAttribute{
...
public ObjectAttribute getCopy{
return copy(this) //return a new instance of himself
}
...
}
Is there a way to ignore the method getCopy() like #JsonIgnoreAttribute("objectProperty.copy")?
For this specific use case, when you have a class in a third party library that you are not able to modify, Jackson provides the Mix-in annotations.
The idea behind this concept is to provide a class that indicates how the serialization of another class should be accomplished.
For instance, consider the following mix-in class definition for your use case:
public abstract class ObjectAttributeMixIn{
// You need to provide definitions for every property you need
// to serialize, and the proper constructor if necessary
...
// Ignore the getCopy method
#JsonIgnore
public abstract ObjectAttribute getCopy();
...
}
You can use the full set of Jackson annotations in the mix-in definitions.
Then, associate the mix-in with the ObjectAttribute class. You can use the instance of ObjectMapper you are using for serialization for this purpose:
objectMapper.addMixInAnnotations(ObjectAttribute.class, ObjectAttributeMixIn.class);
Yon can also register a custom module instead; please, see the relevant documentation.
for ignore method getCopy, just enough rename this method , e.g copy
every method start with get then serialized ,e.g if method name is getSomething then serialized to something: (return value by method))
so if you change method name to copy or copyInstance or every name without start by get then method not serialized
You can override JsonSerializer and do specific logic for class
public class CustomSerializerForC extends JsonSerializer<C> {
#Override
public Class<C> handledType() {
return C.class;
}
#Override
public void serialize(C c, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
String upperCase = c.getValue().toUpperCase();
jsonGenerator.writeString(upperCase);
}
}
And use Serializer in moudle used in ObjectMapper:
SimpleModule module = new SimpleModule("MyCustomModule", new Version(1, 0, 0, null));
module.addSerializer(new CustomSerializerForC());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
There are 2 ways I see how to figure out your issue:
Write custom deserializer for you specific class and register it in Jackson mapper.
Tune up global Jackson mapper to ignore class getters in auto-detection and use only fields.
Please try 2 way with following config:
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(JsonMethod.ALL, Visibility.NONE);
mapper.setVisibility(JsonMethod.FIELD, Visibility.ANY);
If you decide to move forward with 1 way, please write here if you need help.
You can register serializer and choose the fields you would like
/**
* We can not change source code so we are adding serializer for a specific type.
*
*/
public static class JsonSpecificTypeSerializer extends JsonSerializer<SpecificType> {
#Override
public void serialize(SpecificType t, JsonGenerator jsonGen, SerializerProvider serializerProvider) throws IOException {
jsonGen.writeStartObject();
jsonGen.writeFieldName("field1");
jsonGen.writeNumber(t.getield1());
.......
jsonGen.writeEndObject();
}
}
/**
* Customize jackson.
*
* adding configuration to jackson without overriding spring boot default conf.
*/
#Bean
public Jackson2ObjectMapperBuilderCustomizer customizeJackson() {
return jacksonObjectMapperBuilder -> {
jacksonObjectMapperBuilder.serializerByType(SpecificType.class,
new JsonSpecificTypeSerializer());
};
}

How to force Spring Data to create query methods with entity runtime type?

I've got around 5 objects that I want to do similar things with.
I figured out that not to polute the code I will put a logic for those objects in one place.
public class MetaObjectController<T extends MetaObject> {
#Autowired
private final MetaObjectRepository<T> repository;
// generic logic
Here's how repository looks:
public interface MetaObjectRepository<T extends MetaObject> extends GraphRepository<T> {
T findByName(String name);
}
Now, I create concrete class which uses delegation:
public class ExperimentalController {
#Autowired
private final MetaObjectController<MetaCategory> metaController;
#RequestMapping(method = RequestMethod.POST)
public void add(#RequestBody MetaCategory toAdd) {
metaController.add(toAdd);
}
Now, when I look at the generated queries I see, that although instantiated correctly, repository puts MetaObject as an entity name instead of runtime type.
Is there a way to force the repository to use runtime type?
Please don't advise to put a #Query annnotation. That's not what I am looking for.
This is most probably due to type erasure: at runtime there is only the type constraint available which is MetaObject. If you want to use (via spring-data) the actually relevant subclass you will have to create explicit interfaces of the MetaObjectRepository like this:
public class Transmogrifier extends MetaObject
public interface MetaTransmogrifierRepository
extends MetaObjectRepository<Transmogrifier> {}

Overriding #JsonDeserialize and #JsonSerialize Behaviour

I have a Spring(4.1.6) MVC project, properties annotated with #JsonDeserialize and #JsonSerialize in class Foo are working fine. Foo is used within a RestController hence managed by Rest calls.
Foo is packed within a common module hence need to be reused in other modules.
Something like:
-Web
--Common
-Services
--Common(Common is used in both)
we will be using ObjectMapper for conversion in Services module.But some how we need to override behavior so that #JsonDeserialize and #JsonSerialize are ignored in Services module and we get values as is.
One option I can think of is creating new bean extends Foo and overriding annotated properties.
Any pointers to other simple way of doing the same?
I got a solution within Mixin provided by jackson.
We can override Annotations using mixin as shown below for Deserialization(Serialization would be similar):
Step 1. Create a DummyDateDeSerializer, where we can write custom logic for date parsing.In my case I returned date as received.
Step 2. Create a Mixin Class defining properties for which anootations should be overriden.
public abstract class DateMixin {
#JsonDeserialize(using=com.test.jackson.DummyDateDeSerializer.class)
public abstract Date getLastModifiedDate() ;
#JsonDeserialize(using=com.test.jackson.DummyDateDeSerializer.class)
public abstract Date getCreatedDate() ;
}
Step 3.Create a DummyDateModule
public class DummyDateModule extends SimpleModule {
public DummyDateModule() {
super("DummyDateModule", new Version(0, 0, 1, null));
}
#Override
public void setupModule(SetupContext context) {
context.setMixInAnnotations(Foo.class, DateMixin.class);
}
}
Step 4. Register mdoule
private static void updateMapper(ObjectMapper mapper){
mapper.registerModule(new DummyDateModule());
}
This will override any #JsonDeserialize defined in Foo or its super class(es) with DummyDateDesrializer for properties createdDate and lastModifiedDate.

DynamoDBMapper mapping composite objects implementing an interface

Consider an interface with several implementations
Public interface A {}
#DynamoDBDocument
Public class C1 implements A {
variables…
}
#DynamoDBDocument
Public class C2 implements A {
variables…
}
#DynamoDBTable
Public class TopLevelClass {
A obj; //It can be any implementation C1, C2 ….
//Getter and setter of A
}
Saving an object of type C1 in the dynamoDB works fine, but we are unable to deserialize it back. Jackson does it by adding a #class attribute to the json to help it figure out which implementation to instantiate, however DynamoDBMapper has no idea and ends up trying to instantiate an object of A which obviously fails as it’s just an interface.
Is there a way we can achieve this? We don’t want to use marshallers as it will just deserialize the whole object as string and defeats the purpose of DynamoDBDocument type.

Categories

Resources