Issue
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.
Solution
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:
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:
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).
Answered By - Nemanja
Answer Checked By - Marilyn (JavaFixing Volunteer)