Jackson custom serializers

341 views
Skip to first unread message

Matteo

unread,
Aug 8, 2017, 7:23:15 PM8/8/17
to jackson-user
Hello!

I'm trying to write a custom serializer that can transform the following structure

{
   
"a": {
       
"@context": "context-a",
       
"aKey": "aValue"
   
},
   
"b": {
       
"@context": "context-b",
       
"anotherbKey": "anotherbValue",
       
"bKey": "bValue"
   
}
}

into this:

{
   
"@context" : ["context-a", "context-b"],
   
"a": {
       
"aKey": "aValue"
   
},
   
"b": {
       
"anotherbKey": "anotherbValue",
       
"bKey": "bValue"
   
}
}

The reason to do this is to put all json-ld headers at the beginning of the serialized json. I have a utility class that extract all @context attribute from a bean hierarchy (ContextsCrawler in the snippet below) and my current serializer attempt is this:

public class JsonLdModelSerializer extends JsonSerializer<Object> {

   
private static Optional<String> baseContext;
   
private static ContextsCrawler ctxCrawler = new ContextsCrawler();

   
public static void scanClassForContexts(Map<Class<?>, Class<?>> mixins) {
        ctxCrawler
.scanClassForContexts(mixins);
   
}
   
   
public static void setBaseContext(String baseContext) {
       
JsonLdModelSerializer.baseContext = Optional.of(baseContext);
   
}

   
@Override
   
public void serialize(Object value, JsonGenerator jgen, SerializerProvider serializers)
           
throws IOException, JsonProcessingException {
       
        jgen
.writeStartObject();
       
// Add contexts elements in top level element only:
       
if (jgen.getOutputContext().getParent().inRoot()) {
           
Collection<String> cxts = ctxCrawler.getContexts(value);
           
if (cxts != null) {
               
Set<String> ctxset = new HashSet<>();
                baseContext
.ifPresent(ctx -> ctxset.add(ctx));
                ctxset
.addAll(cxts);
                jgen
.writeObjectField("@context", ctxset);
           
}
       
}
       
JavaType javaType = serializers.constructType(value.getClass());
       
BeanDescription beanDesc = serializers.getConfig().introspect(javaType);
       
JsonSerializer<Object> serializer = BeanSerializerFactory.instance.findBeanSerializer(serializers,
                javaType
,
                beanDesc
);
        serializer
.unwrappingSerializer(null).serialize(value, jgen, serializers);

        jgen
.writeEndObject();
   
}
}


Then I simply define a mixin for each class that contains a @context element like this:

@JsonSerialize(using = JsonLdModelSerializer.class)
@JsonIgnoreProperties("context")
public class CapabilityMixIn extends Capability {
 
@Override
 
@JsonLdContextProvider // used by ContextsCrawler
 
public List<String> getContext() {
 
return super.getContext();
 
}
}

It works but it has two main problems:

1. I don't know it the direct usage of BeanSerializerFactory in serialize method is OK since I read that this is not the best approach to use. I'm also worried about performance implications;
2. For some reason this serializer doesn't play nice with beans that contains Map<String,Object> that are decorated with @JsonAnyGetter: the resulting json does not contain the map elements

Could you please provide guidance? What is the right approach to implement such a JsonLdModelSerializer? 

Thank you,
Matteo 

Tatu Saloranta

unread,
Aug 9, 2017, 3:44:59 PM8/9/17
to jackson-user
Ok, that is quite an elaborate setup.

So... typically I recommend multi-phase processing for most structural
changes: first "serialize" content into `JsonNode` using
`ObjectMapper.valueToTree()`; then transform tree, and finally
serialize the modified tree as JSON.
This is especially true for cases where you need to apply
transformations for all kinds of types, not just specific classes.
Serializer/deserializer setup is designed more for strict(er) typing,
and as such is not necessarily good at applying things for all types.

But it is certainly possible to do this via serializers too. Usually I
would have looked at `StdDelegatingSerializer`, in which you can take
a specific type (that Jackson does not know how to properly
serialize), and construct alternative value (like `JsonNode` or `Map`)
and let Jackson serialize that instead. This is somewhat similar to
use of `@JsonValue` annotation, in which POJO is serialized using sort
of surrogate.
But I am not sure this approach would work here, since this is applied
to any types it seems (`Object`), and since you may want to operate on
outputs of standard Bean-style serialization.

As to use of `BeanSerializerFactory`: you are correct in suspecting
this is not the way to do it. It is not. :-)

Instead you would ideally do one of:

1. If types are statically known, implement `ContextualSerializer`, in
which you can get access to the "standard" serializer, keep reference
to it, add your own implementation that may call original one
(including giving it `TokenBuffer` as `JsonGenerator`!).
2. If types not known until actual serialization (dynamically) --
which I think is what you have here -- find serializer(s) via
`SerializerProvider`. It will call factory, as well as possible
extension modules, to find and initialize serializer.

I can help with (2) if you have hard time finding method to call; for
now I assume you can find it ok.

I hope this helps,

-+ Tatu +-

Matteo

unread,
Aug 10, 2017, 5:00:13 PM8/10/17
to jackson-user
Dear Tatu,
I really thank you for your immediate and very thorough answer, I really appreciate!

I tried with the SerializerProvider approach but I was not able to implement something that works. Could you please point me to the right direction with the approach no.(2)? Is there any simple example I can start with in the jackson codebase?

Thank you!

matteo

Tatu Saloranta

unread,
Aug 10, 2017, 6:03:36 PM8/10/17
to jackson-user
On Thu, Aug 10, 2017 at 2:00 PM, Matteo <matteo...@gmail.com> wrote:
> Dear Tatu,
> I really thank you for your immediate and very thorough answer, I really
> appreciate!
>
> I tried with the SerializerProvider approach but I was not able to implement
> something that works. Could you please point me to the right direction with
> the approach no.(2)? Is there any simple example I can start with in the
> jackson codebase?

Most serializers resolve dependant types in `createContextual()`, but
method to call is typically
`findValueSerializer()`. If possible, you would still define
`createContextual()` to get hold of `BeanProperty` in question (which
may or may not have annotations that might affect behavior of
serializer), to pass that. If that is of no consequence however can
use variant that only takes `Class<?>` (or `JavaType`, if needed for
generic value type).

Other variants (`findTypeValueSerializer()`) are relevant for
polymorphic handling.

Regardless of which method you call, use of resulting serializer
should be the same as in your original example.

-+ Tatu +-
> --
> You received this message because you are subscribed to the Google Groups
> "jackson-user" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to jackson-user...@googlegroups.com.
> To post to this group, send email to jackso...@googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.
Reply all
Reply to author
Forward
0 new messages