1

In jackson, we can uses the annotations

  1. @JsonTypeInfo
  2. @JsonSubTypes
  3. @JsonSubTypes.Type

to implement polymorphic serialization.

We can choose to

  1. Use these annotations on data model directly, this is the simplest way.
  2. 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

  1. The discriminator field is fixed: "__typename"
  2. 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.

Tao Chen
  • 197
  • 10

2 Answers2

0

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));
dariosicily
  • 4,239
  • 2
  • 11
  • 17
  • Thanks for so long reply. Sorry, I forget one point. This is my current solution. Goal: Like the default polymorphic behavior of jackson, that field only be added if necessary. ``` var store1 = new BookStore(); store1.setBooks(List.of(new Book(), new Book())); var store2 = new BookStore(); store2.setBooks(List.of(new ElectronicBook(), new Book())); ``` store1, all objects match generic argument of List, unnecessary. store2, not match that generic argument, so necessary. Apologies again for not describing the problem clearly. – Tao Chen Jan 11 '22 at 20:40
0

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

Tao Chen
  • 197
  • 10