Home > Mobile >  Specify jackson writter schema
Specify jackson writter schema

Time:08-31

Is it possible to specify a schema for the writing part of Jackson with springboot.

For instance:

I have two pojo classes.

class A {
    int a;
    B b;

    public A() { }
    
    // Getter and Setter

}

class B {
    double d;
    String s;

    public B() { }
    
    // Getter and Setter

}

And I have a service


public SomeClass {
    @RequestMapping(path = "/mapping", method = RequestMethod.POST)
    public A recupererPESRetourDepuisPlateformeBLES()
        return callToSomeMethodThatReturnsA();
    }
}

Is there a way to specify that for the return type I want:

the attribute a and b of my object A and the attribute s of the object B.

I want the client to somehow send what we want to the server and the server parses the schema required and returns only it.

I know about @JsonIgnore and @JsonProperty. I also know GraphQL but I want to stay with Jackson.

Update 1

Example:

I have instantiated in Java such a structure (here represented in JSON to simplify)

"A" {
    "a": 12,
    "b": {
        "d": 23.362,
        "s": "Hello world"
    }
}

After my request, I want the server to send to the client:

"A" {
    "a": 12,
    "b": {
        "s": "Hello world"
    }
}

I don't know what kind of data my client can send to my server to specify the schema of data I want as output. This is part of the question.

CodePudding user response:

Spring boot dynamic filtering concept should help in this case. Modify the return type of the method recupererPESRetourDepuisPlateformeBLES() to MappingJacksonValue . Here are the code changes needed to be made:

import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

public SomeClass {
    @RequestMapping(path = "/mapping", method = RequestMethod.POST)
    public MappingJacksonValue recupererPESRetourDepuisPlateformeBLES()
        SimpleBeanPropertyFilter propertyFilter = SimpleBeanPropertyFilter.filterOutAllExcept("s");
        FilterProvider filterProvider = new SimpleFilterProvider().addFilter("dynamicFilter", propertyFilter);


        B b = new B();
        b.setD(1d);
        b.setS("someString");

        A a = new A();
        a.setA(1);
        a.setB(b);

        MappingJacksonValue value = new MappingJacksonValue(a);
        value.setFilters(filterProvider);
        return value;
    }
}

Annotate the target Response class with @JsonFilter like below

import com.fasterxml.jackson.annotation.JsonFilter;   
@JsonFilter("dynamicFilter")
public class B {
    double d;
    String s;

    public B() {
    }
}

Hope. This should solve the problem.

CodePudding user response:

I assume that you want to be able to do deep filtering ( be able to filter based on properties of classes A and B). To achieve that you can use following filter:

package com.example.demo.filter;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonStreamContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.ser.PropertyFilter;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;

import java.util.Set;

/**
 * Sample filtering fields durring json marshalling.
 */
public class JSON {

    private static final String DEFAULT_FILTER = "__default";
    private static final String DOT = ".";

    private static final ObjectMapper MAPPER = new ObjectMapper().setAnnotationIntrospector(
            new AnnotationIntrospectorPair(
                    new FilteringAnnotationInpector(), new JaxbAnnotationIntrospector(TypeFactory.defaultInstance())
            )
    );

    public static String asString(Object object, Set<String> fields) {
        PropertyFilter filter = filter(fields);
        SimpleFilterProvider provider = new SimpleFilterProvider();
        provider.addFilter(DEFAULT_FILTER, filter);
        try {
            return MAPPER.writer(provider).writeValueAsString(object);
        } catch (JsonProcessingException ex) {
            throw new RuntimeException("failed to marshall", ex);
        }
    }

    private static PropertyFilter filter(Set<String> fields) {
        PropertyFilter filter;
        if (fields.size() > 0) {

            filter = new DeepFieldFilter(fields);
        } else {
            filter = SimpleBeanPropertyFilter.serializeAll();
        }
        return filter;
    }

    private static class FilteringAnnotationInpector extends JacksonAnnotationIntrospector {

        private static final long serialVersionUID = -8722016441050379430L;

        @Override
        public String findFilterId(Annotated a) {
            return DEFAULT_FILTER;
        }

    }

    private static class DeepFieldFilter extends SimpleBeanPropertyFilter {

        private final Set<String> includes;

        private DeepFieldFilter(Set<String> includes) {
            this.includes = includes;
        }

        private String createPath(PropertyWriter writer, JsonGenerator jgen) {
            StringBuilder path = new StringBuilder();
            path.append(writer.getName());
            JsonStreamContext sc = jgen.getOutputContext();
            if (sc != null) {
                sc = sc.getParent();
            }

            while (sc != null) {
                if (sc.getCurrentName() != null) {
                    if (path.length() > 0) {
                        path.insert(0, DOT);
                    }
                    path.insert(0, sc.getCurrentName());
                }
                sc = sc.getParent();
            }
            return path.toString();
        }

        @Override
        public void serializeAsField(Object pojo, JsonGenerator gen, SerializerProvider provider, PropertyWriter writer)
                throws Exception {
            String path = createPath(writer, gen);
            if (includes.contains(path)) {
                writer.serializeAsField(pojo, gen, provider);
            } else {
                writer.serializeAsOmittedField(pojo, gen, provider);
            }
        }

    }
}

Here is complete example how to use this.

So, for example, if you want to return only property "a" of object A, first you will create controller method like this:

 @GetMapping("/test")
    public String test(@RequestBody Set<String> filterFields)  {

        B b = new B();
        b.setD(23);
        b.setS("b test");
        A a = new A();
        a.setA(1);
        a.setB(b);


        return JSON.asString(a, filterFields);
    }

And if you send request like this:

enter image description here

The output should be {"a":1}

If you want to display property b of A class, with field s, you will send request like this:

enter image description here

And the output shoud be {"b":{"s":"b test"}}

As far as I can see, there is no other way to achieve what you want (using just Jackson).

  • Related