I am writing a rest service using spring MVC which produces JSON response. It should allow client to select only the given fields in response, means client can mention the fields he is interested in as url parameter like ?fields=field1,field2.
Using Jackson annotations does not provide what I am looking for as it is not dynamic also the filters in Jackson doesnt seem to be promising enough.
So far I am thinking to implement a custom message converter which can take care of this.
Is there any other better way to achieve this? I would like if this logic is not coupled with my services or controllers.
From Spring 4.2, #JsonFilter is supported in MappingJacksonValue
Issue : SPR-12586 : Support Jackson #JsonFilter
Commit : ca06582
You can directly inject PropertyFilter to MappingJacksonValue in a controller.
#RestController
public class BookController {
private static final String INCLUSION_FILTER = "inclusion";
#RequestMapping("/novels")
public MappingJacksonValue novel(String[] include) {
#JsonFilter(INCLUSION_FILTER)
class Novel extends Book {}
Novel novel = new Novel();
novel.setId(3);
novel.setTitle("Last summer");
novel.setAuthor("M.K");
MappingJacksonValue res = new MappingJacksonValue(novel);
PropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept(include);
FilterProvider provider = new SimpleFilterProvider().addFilter(INCLUSION_FILTER, filter);
res.setFilters(provider);
return res;
}
or you can declare global policy by ResponseBodyAdvice. The following example implements filtering policy by "exclude" parameter.
#ControllerAdvice
public class DynamicJsonResponseAdvice extends AbstractMappingJacksonResponseBodyAdvice {
public static final String EXCLUDE_FILTER_ID = "dynamicExclude";
private static final String WEB_PARAM_NAME = "exclude";
private static final String DELI = ",";
private static final String[] EMPTY = new String[]{};
#Override
protected void beforeBodyWriteInternal(MappingJacksonValue container, MediaType contentType,
MethodParameter returnType, ServerHttpRequest req, ServerHttpResponse res) {
if (container.getFilters() != null ) {
// It will be better to merge FilterProvider
// If 'SimpleFilterProvider.addAll(FilterProvider)' is provided in Jackson, it will be easier.
// But it isn't supported yet.
return;
}
HttpServletRequest baseReq = ((ServletServerHttpRequest) req).getServletRequest();
String exclusion = baseReq.getParameter(WEB_PARAM_NAME);
String[] attrs = StringUtils.split(exclusion, DELI);
container.setFilters(configFilters(attrs));
}
private FilterProvider configFilters(String[] attrs) {
String[] ignored = (attrs == null) ? EMPTY : attrs;
PropertyFilter filter = SimpleBeanPropertyFilter.serializeAllExcept(ignored);
return new SimpleFilterProvider().addFilter(EXCLUDE_FILTER_ID, filter);
}
}
IMHO, the simplest way to do that would be to use introspection to dynamically generate a hash containing selected fields and then serialize that hash using Json. You simply have to decide what is the list of usable fields (see below).
Here are two example functions able to do that, first gets all public fields and public getters, the second gets all declared fields (including private ones) in current class and all its parent classes :
public Map<String, Object> getPublicMap(Object obj, List<String> names)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
List<String> gettedFields = new ArrayList<String>();
Map<String, Object> values = new HashMap<String, Object>();
for (Method getter: obj.getClass().getMethods()) {
if (getter.getName().startsWith("get") && (getter.getName().length > 3)) {
String name0 = getter.getName().substring(3);
String name = name0.substring(0, 1).toLowerCase().concat(name0.substring(1));
gettedFields.add(name);
if ((names == null) || names.isEmpty() || names.contains(name)) {
values.put(name, getter.invoke(obj));
}
}
}
for (Field field: obj.getClass().getFields()) {
String name = field.getName();
if ((! gettedFields.contains(name)) && ((names == null) || names.isEmpty() || names.contains(name))) {
values.put(name, field.get(obj));
}
}
return values;
}
public Map<String, Object> getFieldMap(Object obj, List<String> names)
throws IllegalArgumentException, IllegalAccessException {
Map<String, Object> values = new HashMap<String, Object>();
for (Class<?> clazz = obj.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) {
for (Field field : clazz.getDeclaredFields()) {
String name = field.getName();
if ((names == null) || names.isEmpty() || names.contains(name)) {
field.setAccessible(true);
values.put(name, field.get(obj));
}
}
}
return values;
}
Then you only have to get the result of one of this function (or of one you could adapt to your requirements) and serialize it with Jackson.
If you have custom encoding of you domain objects, you would have to maintain the serialization rules in two different places : hash generation and Jackson serialization. In that case, you could simply generate the full class serialization with Jackson and filter the generated string afterwards. Here is an example of such a filter function :
public String jsonSub(String json, List<String> names) throws IOException {
if ((names == null) || names.isEmpty()) {
return json;
}
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> map = mapper.readValue(json, HashMap.class);
for (String name: map.keySet()) {
if (! names.contains(name)) {
map.remove(name);
}
}
return mapper.writeValueAsString(map);
}
Edit : integration in Spring MVC
As you are speaking of a web service and of Jackson, I assume that you use Spring RestController or ResponseBody annotations and (under the hood) a MappingJackson2HttpMessageConverter. If you use Jackson 1 instead, it should be a MappingJacksonHttpMessageConverter.
What I propose is simply to add a new HttpMessageConverter that could make use of one of the above filtering functions, and delegate actual work (and also ancilliary methods) to a true MappingJackson2HttpMessageConverter. In the write method of that new converter, it is possible to have access to the eventual fields request parameter with no need for an explicit ThreadLocal variable thanks to Spring RequestContextHolder. That way :
you keep a clear separation of roles with no modification on existing controllers
you have no modification in Jackson2 configuration
you need no new ThreadLocal variable and simply use a Spring class in a class already tied to Spring since it implements HttpMessageConverter
Here is an example of such a message converter :
public class JsonConverter implements HttpMessageConverter<Object> {
private static final Logger logger = LoggerFactory.getLogger(JsonConverter.class);
// a real message converter that will respond to ancilliary methods and do the actual work
private HttpMessageConverter<Object> delegate =
new MappingJackson2HttpMessageConverter();
// allow configuration of the fields name
private String fieldsParam = "fields";
public void setFieldsParam(String fieldsParam) {
this.fieldsParam = fieldsParam;
}
#Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return delegate.canRead(clazz, mediaType);
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return delegate.canWrite(clazz, mediaType);
}
#Override
public List<MediaType> getSupportedMediaTypes() {
return delegate.getSupportedMediaTypes();
}
#Override
public Object read(Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return delegate.read(clazz, inputMessage);
}
#Override
public void write(Object t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
// is there a fields parameter in request
String[] fields = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest().getParameterValues(fieldsParam);
if (fields != null && fields.length != 0) {
// get required field names
List<String> names = new ArrayList<String>();
for (String field : fields) {
String[] f_names = field.split("\\s*,\\s*");
names.addAll(Arrays.asList(f_names));
}
// special management for Map ...
if (t instanceof Map) {
Map<?, ?> tmap = (Map<?, ?>) t;
Map<String, Object> map = new LinkedHashMap<String, Object>();
for (Entry entry : tmap.entrySet()) {
String name = entry.getKey().toString();
if (names.contains(name)) {
map.put(name, entry.getValue());
}
}
t = map;
} else {
try {
Map<String, Object> map = getMap(t, names);
t = map;
} catch (Exception ex) {
throw new HttpMessageNotWritableException("Error in field extraction", ex);
}
}
}
delegate.write(t, contentType, outputMessage);
}
/**
* Create a Map by keeping only some fields of an object
* #param obj the Object
* #param names names of the fields to keep in result Map
* #return a map containing only requires fields and their value
* #throws IllegalArgumentException
* #throws IllegalAccessException
*/
public static Map<String, Object> getMap(Object obj, List<String> names)
throws IllegalArgumentException, IllegalAccessException {
Map<String, Object> values = new HashMap<String, Object>();
for (Class<?> clazz = obj.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) {
for (Field field : clazz.getDeclaredFields()) {
String name = field.getName();
if (names.contains(name)) {
field.setAccessible(true);
values.put(name, field.get(obj));
}
}
}
return values;
}
}
If you want the converter to be more versatile, you could define an interface
public interface FieldsFilter {
Map<String, Object> getMap(Object obj, List<String> names)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;
}
and inject it with an implementation of that.
Now you must ask Spring MVC to use that custom message controller.
If you use XML config, you simply declare it in the <mvc:annotation-driven> element :
<mvc:annotation-driven >
<mvc:message-converters>
<bean id="jsonConverter" class="org.example.JsonConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
And if you use Java configuration, it is almost as simple :
#EnableWebMvc
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Autowired JsonConverter jsonConv;
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(jsonConv);
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
stringConverter.setWriteAcceptCharset(false);
converters.add(new ByteArrayHttpMessageConverter());
converters.add(stringConverter);
converters.add(new ResourceHttpMessageConverter());
converters.add(new SourceHttpMessageConverter<Source>());
converters.add(new AllEncompassingFormHttpMessageConverter());
converters.add(new MappingJackson2HttpMessageConverter());
}
}
but here you have to explicitely add all the default message converters that you need.
I've never done this but after looking at this page http://wiki.fasterxml.com/JacksonFeatureJsonFilter it seems that it would be possible to do what you want this way:
1) Create a custom JacksonAnnotationIntrospector implementation (by extending default one) that will use a ThreadLocal variable to choose a filter for current request and also create a custom FilterProvider that would provide that filter.
2) Configure the message converter's ObjectMapper to use the custom introspector and filter provider
3) Create an MVC interceptor for REST service that detects fields request parameter and configures a new filter for current request via your custom filter provider (this should be a thread local filter). ObjectMapper should pick it up through your custom JacksonAnnotationIntrospector.
I'm not 100% certain that this solution would be thread safe (it depends on how ObjectMapper uses annotation introspector and filter provider internally).
- EDIT -
Ok I did a test implementation and found out that step 1) wouldn't work because Jackson caches the result of AnnotationInterceptor per class. I modified idea to apply dynamic filtering only on annotated controller methods and only if the object doesn't have anoter JsonFilter already defined.
Here's the solution (it's quite lengthy):
DynamicRequestJsonFilterSupport class manages the per-request fields to be filtered out:
public class DynamicRequestJsonFilterSupport {
public static final String DYNAMIC_FILTER_ID = "___DYNAMIC_FILTER";
private ThreadLocal<Set<String>> filterFields;
private DynamicIntrospector dynamicIntrospector;
private DynamicFilterProvider dynamicFilterProvider;
public DynamicRequestJsonFilterSupport() {
filterFields = new ThreadLocal<Set<String>>();
dynamicFilterProvider = new DynamicFilterProvider(filterFields);
dynamicIntrospector = new DynamicIntrospector();
}
public FilterProvider getFilterProvider() {
return dynamicFilterProvider;
}
public AnnotationIntrospector getAnnotationIntrospector() {
return dynamicIntrospector;
}
public void setFilterFields(Set<String> fieldsToFilter) {
filterFields.set(Collections.unmodifiableSet(new HashSet<String>(fieldsToFilter)));
}
public void setFilterFields(String... fieldsToFilter) {
filterFields.set(Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(fieldsToFilter))));
}
public void clear() {
filterFields.remove();
}
public static class DynamicIntrospector extends JacksonAnnotationIntrospector {
#Override
public Object findFilterId(Annotated annotated) {
Object result = super.findFilterId(annotated);
if (result != null) {
return result;
} else {
return DYNAMIC_FILTER_ID;
}
}
}
public static class DynamicFilterProvider extends FilterProvider {
private ThreadLocal<Set<String>> filterFields;
public DynamicFilterProvider(ThreadLocal<Set<String>> filterFields) {
this.filterFields = filterFields;
}
#Override
public BeanPropertyFilter findFilter(Object filterId) {
return null;
}
#Override
public PropertyFilter findPropertyFilter(Object filterId, Object valueToFilter) {
if (filterId.equals(DYNAMIC_FILTER_ID) && filterFields.get() != null) {
return SimpleBeanPropertyFilter.filterOutAllExcept(filterFields.get());
}
return super.findPropertyFilter(filterId, valueToFilter);
}
}
}
JsonFilterInterceptor intercepts controller methods annotated with custom #ResponseFilter annotation.
public class JsonFilterInterceptor implements HandlerInterceptor {
#Autowired
private DynamicRequestJsonFilterSupport filterSupport;
private ThreadLocal<Boolean> requiresReset = new ThreadLocal<Boolean>();
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
ResponseFilter filter = method.getMethodAnnotation(ResponseFilter.class);
String[] value = filter.value();
String param = filter.param();
if (value != null && value.length > 0) {
filterSupport.setFilterFields(value);
requiresReset.set(true);
} else if (param != null && param.length() > 0) {
String filterParamValue = request.getParameter(param);
if (filterParamValue != null) {
filterSupport.setFilterFields(filterParamValue.split(","));
}
}
}
requiresReset.remove();
return true;
}
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
Boolean reset = requiresReset.get();
if (reset != null && reset) {
filterSupport.clear();
}
}
}
Here's the custom #ResponseFilter annotation. You can either define a static filter (via annotation's value property) or a filter based on request param (via annotation's param property):
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface ResponseFilter {
String[] value() default {};
String param() default "";
}
You will need to setup the message converter and the interceptor in the config class:
...
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(converter());
}
#Bean
JsonFilterInterceptor jsonFilterInterceptor() {
return new JsonFilterInterceptor();
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jsonFilterInterceptor);
}
#Bean
DynamicRequestJsonFilterSupport filterSupport() {
return new DynamicRequestJsonFilterSupport();
}
#Bean
MappingJackson2HttpMessageConverter converter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(filterSupport.getAnnotationIntrospector());
mapper.setFilters(filterSupport.getFilterProvider());
converter.setObjectMapper(mapper);
return converter;
}
...
And finally, you can use the filter like this:
#RequestMapping("/{id}")
#ResponseFilter(param = "fields")
public Invoice getInvoice(#PathVariable("id") Long id) { ... }
When request is made to /invoices/1?fields=id,number response will be
filtered and only id and number properties will be returned.
Please note I haven't tested this thoroughly but it should get you started.
Would populating a HashMap from the object not suite the requirements? You could then just parse the HashMap. I have done something similar with GSON in the past where I had to provide a simple entity and ended up just populating a HashMap and then serializing it, it was far more maintainable than over engineering a whole new system.
Related
I have an entity with multiple #ManyToOne associations. I am using spring-boot to expose a REST API. Currently, I have multiple REST API's which return a JSON response of the whole entity, including associations.
But I don't want to serialize all associated objects in all REST APIs.
For example
API-1 should return parent + associationA object
API-2 should return parent + associationA + associationB object
API-3 should return parent + associationB + associationC + associationD
So, in my serialization process, I want to ignore all association except associationA for API-1.
For API-2 I want to ignore other associations except A and B
How do I dynamically ignore these properties during Jackson serialization?
Notes:
I'm using the same class for each; I am not interested in creating a DTO for each API.
Any suggestions are kingly appreciated.
I've put together three approaches for performing dynamic filtering in Jackson. One of them must suit your needs.
Using #JsonView
You could use #JsonView:
public class Views {
interface Simple { }
interface Detailed extends Simple { }
}
public class Foo {
#JsonView(Views.Simple.class)
private String name;
#JsonView(Views.Detailed.class)
private String details;
// Getters and setters
}
#RequestMapping("/foo")
#JsonView(Views.Detailed.class)
public Foo getFoo() {
Foo foo = new Foo();
return foo;
}
Alternatively you can set the view dynamically with MappingJacksonValue.
#RequestMapping("/foo")
public MappingJacksonValue getFoo() {
Foo foo = new Foo();
MappingJacksonValue result = new MappingJacksonValue(foo);
result.setSerializationView(Views.Detailed.class);
return result;
}
Using a BeanSerializerModifier
You could extend BeanSerializerModifier and then override the changeProperties() method. It allows you to add, remove or replace any of properties for serialization, according to your needs:
public class CustomSerializerModifier extends BeanSerializerModifier {
#Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config,
BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
// In this method you can add, remove or replace any of passed properties
return beanProperties;
}
}
Then register the serializer as a module in your ObjectMapper:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new SimpleModule() {
#Override
public void setupModule(SetupContext context) {
super.setupModule(context);
context.addBeanSerializerModifier(new CustomSerializerModifier());
}
});
Check examples here and here.
Using #JsonFilter with a SimpleBeanPropertyFilter
Another approach involves #JsonFilter:
#JsonFilter("customPropertyFilter")
public class Foo {
private String name;
private String details;
// Getters and setters
}
Extend SimpleBeanPropertyFilter and override the serializeAsField() method according to your needs:
public class CustomPropertyFilter extends SimpleBeanPropertyFilter {
#Override
public void serializeAsField(Object pojo, JsonGenerator jgen,
SerializerProvider provider,
PropertyWriter writer) throws Exception {
// Serialize a field
// writer.serializeAsField(pojo, jgen, provider, writer);
// Omit a field from serialization
// writer.serializeAsOmittedField(pojo, jgen, provider);
}
}
Then register the filter in your ObjectMapper:
FilterProvider filterProvider = new SimpleFilterProvider()
.addFilter("customPropertyFilter", new CustomPropertyFilter());
ObjectMapper mapper = new ObjectMapper();
mapper.setFilterProvider(filterProvider);
If you want to make your filter "global", that is, to be applied to all beans, you can create a mix-in class and annotate it with #JsonFilter("customPropertyFilter"):
#JsonFilter("customPropertyFilter")
public class CustomPropertyFilterMixIn {
}
Then bind the mix-in class to Object:
mapper.addMixIn(Object.class, CustomPropertyFilterMixIn.class);
public static <T> String getNonNullFieldsSerialized(T object, ObjectMapper objectMapper)throws JsonProcessingException {
Map<String, Object> objectMap = objectMapper.convertValue(object, new TypeReference<Map<String, Object>>() {});
Map<String, Object> objectMapNonNullValues = objectMap.entrySet().stream()
.filter(stringObjectEntry -> Objects.nonNull(stringObjectEntry.getValue()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
return objectMapper.writeValueAsString(objectMapNonNullValues);
}
This will basically ignore all the fields that are non-null. Similarly you can ignore other fields by changing the map filter condition.
I have implemented dynamic filter on data getting from db and returning it using rest api.I have avoided using MappingJacksonValue.As it was getting issue while object chaining
#GetMapping("/courses")
public ResponseEntity<JpaResponse> allCourse() throws Exception {
JpaResponse response = null;
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
List<Course> course = service.findAllCourse();
SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept("name","reviews");
FilterProvider filterProvider = new SimpleFilterProvider().addFilter("jpafilter", filter).setFailOnUnknownId(false);
ObjectWriter writer = mapper.writer(filterProvider);
String writeValueAsString = writer.writeValueAsString(course);
List<Course> resultcourse = mapper.readValue(writeValueAsString,List.class);
response = new JpaResponse(HttpStatus.OK.name(),resultcourse);
return new ResponseEntity<>(response, HttpStatus.OK);
}
public class JpaResponse {
private String status;
private Object data;
public JpaResponse() {
super();
}
public JpaResponse(String status, Object data) {
super();
this.status = status;
this.data = data;
}
}
I want to pretty print json responses from Spring MVC Restcontrollers dynamically based on a http parameter (like suggested here: http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api#pretty-print-gzip).
I have found configurations for pretty print it by static configuration, but not how to do that dynamically?
When using Spring MVC for REST, how do you enable Jackson to pretty-print rendered JSON?
Any idea how to do that?
Introducing A New Media Type
You can define a new Media Type, say, application/pretty+json and register a new HttpMessageConverter that converts to that Media Type. In fact, if client sends a request with Accept: application/pretty+json header, our new HttpMessageConverter will write the response, Otherwise, the plain old MappingJackson2HttpMessageConverter would do that.
So, extends the MappingJackson2HttpMessageConverter like following:
public class PrettyPrintJsonConverter extends MappingJackson2HttpMessageConverter {
public PrettyPrintJsonConverter() {
setPrettyPrint(true);
}
#Override
public List<MediaType> getSupportedMediaTypes() {
return Collections.singletonList(new MediaType("application", "pretty+json"));
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
boolean canWrite = super.canWrite(clazz, mediaType);
boolean canWritePrettily = mediaType != null &&
mediaType.getSubtype().equals("pretty+json");
return canWrite && canWritePrettily;
}
}
That setPrettyPrint(true) in constructor will do the trick for us. Then we should register this HttpMessageConverter:
#EnableWebMvc
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new PrettyPrintJsonConverter());
}
}
As i said, if client send a request with application/pretty+json Accept header, our PrettyPrintJsonConverter will write the JSON representation Prettily. Otherwise, MappingJackson2HttpMessageConverter would write a compact JSON to the response body.
You can achieve the same with a ResponseBodyAdvice or even Interceptors but in my opinion, registering a brand new HttpMessageConverter is the better approach.
To switch to pretty rendering with a ?pretty=true parameter I use a custom MappingJackson2HttpMessageConverter
#Configuration
#RestController
public class MyController {
#Bean
MappingJackson2HttpMessageConverter currentMappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter jsonConverter = new CustomMappingJackson2HttpMessageConverter();
return jsonConverter;
}
public static class Input {
public String pretty;
}
public static class Output {
#JsonIgnore
public String pretty;
}
#RequestMapping(path = "/api/test", method = {RequestMethod.GET, RequestMethod.POST})
Output test( #RequestBody(required = false) Input input,
#RequestParam(required = false, value = "pretty") String pretty)
{
if (input.pretty==null) input.pretty = pretty;
Output output = new Output();
output.pretty = input.pretty;
return output;
}
}
The converter :
public class CustomMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
ObjectMapper objectMapper;
ObjectMapper prettyPrintObjectMapper;
public CustomMappingJackson2HttpMessageConverter() {
objectMapper = new ObjectMapper();
prettyPrintObjectMapper = new ObjectMapper();
prettyPrintObjectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
}
#Override
#SuppressWarnings("deprecation")
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
try {
writePrefix(generator, object);
Class<?> serializationView = null;
FilterProvider filters = null;
Object value = object;
JavaType javaType = null;
if (object instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) object;
value = container.getValue();
serializationView = container.getSerializationView();
filters = container.getFilters();
}
javaType = getJavaType(type, null);
ObjectMapper currentMapper = objectMapper;
Field prettyField = ReflectionUtils.findField(object.getClass(), "pretty");
if (prettyField != null) {
Object prettyObject = ReflectionUtils.getField(prettyField, object);
if (prettyObject != null && prettyObject instanceof String) {
String pretty = (String)prettyObject;
if (pretty.equals("true"))
currentMapper = prettyPrintObjectMapper;
}
}
ObjectWriter objectWriter;
if (serializationView != null) {
objectWriter = currentMapper.writerWithView(serializationView);
}
else if (filters != null) {
objectWriter = currentMapper.writer(filters);
}
else {
objectWriter = currentMapper.writer();
}
if (javaType != null && javaType.isContainerType()) {
objectWriter = objectWriter.withType(javaType);
}
objectWriter.writeValue(generator, value);
writeSuffix(generator, object);
generator.flush();
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write content: " + ex.getMessage(), ex);
}
}
}
Franck
I like Franck Lefebure's approach, but i don't like used reflection, so here is a solution with using custom PrettyFormattedBody type + pretty formatted arrays/lists:
Spring Config:
#Bean
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
return new CustomJsonResponseMapper();
}
CustomJsonResponseMapper.java:
public class CustomJsonResponseMapper extends MappingJackson2HttpMessageConverter {
private final ObjectMapper prettyPrintObjectMapper;
public CustomJsonResponseMapper() {
super();
prettyPrintObjectMapper = initiatePrettyObjectMapper();
}
protected ObjectMapper initiatePrettyObjectMapper() {
// clone and re-configure default object mapper
final ObjectMapper prettyObjectMapper = objectMapper != null ? objectMapper.copy() : new ObjectMapper();
prettyObjectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
// for arrays - use new line for every entry
DefaultPrettyPrinter pp = new DefaultPrettyPrinter();
pp.indentArraysWith(new DefaultIndenter());
prettyObjectMapper.setDefaultPrettyPrinter(pp);
return prettyObjectMapper;
}
#Override
protected void writeInternal(final Object objectToWrite, final Type type, final HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
// based on: if objectToWrite is PrettyFormattedBody with isPretty == true => use custom formatter
// otherwise - use the default one
final Optional<PrettyFormattedBody> prettyFormatted = Optional.ofNullable(objectToWrite)
.filter(o -> o instanceof PrettyFormattedBody)
.map(o -> (PrettyFormattedBody) objectToWrite);
final boolean pretty = prettyFormatted.map(PrettyFormattedBody::isPretty).orElse(false);
final Object realObject = prettyFormatted.map(PrettyFormattedBody::getBody).orElse(objectToWrite);
if (pretty) {
// this is basically full copy of super.writeInternal(), but with custom (pretty) object mapper
MediaType contentType = outputMessage.getHeaders().getContentType();
JsonEncoding encoding = getJsonEncoding(contentType);
JsonGenerator generator = this.prettyPrintObjectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
try {
writePrefix(generator, realObject);
Class<?> serializationView = null;
FilterProvider filters = null;
Object value = realObject;
JavaType javaType = null;
if (realObject instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) realObject;
value = container.getValue();
serializationView = container.getSerializationView();
filters = container.getFilters();
}
if (type != null && value != null && TypeUtils.isAssignable(type, value.getClass())) {
javaType = getJavaType(type, null);
}
ObjectWriter objectWriter;
if (serializationView != null) {
objectWriter = this.prettyPrintObjectMapper.writerWithView(serializationView);
} else if (filters != null) {
objectWriter = this.prettyPrintObjectMapper.writer(filters);
} else {
objectWriter = this.prettyPrintObjectMapper.writer();
}
if (javaType != null && javaType.isContainerType()) {
objectWriter = objectWriter.forType(javaType);
}
objectWriter.writeValue(generator, value);
writeSuffix(generator, realObject);
generator.flush();
} catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex);
}
} else {
// use default formatting if isPretty property is not specified
super.writeInternal(realObject, type, outputMessage);
}
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
// this should be mandatory overridden,
// otherwise writeInternal() won't be called with custom PrettyFormattedBody type
return (PrettyFormattedBody.class.equals(clazz) && canWrite(mediaType)) || super.canWrite(clazz, mediaType);
}
public static final class PrettyFormattedBody {
private final Object body;
private final boolean pretty;
public PrettyFormattedBody(Object body, boolean pretty) {
this.body = body;
this.pretty = pretty;
}
public Object getBody() {
return body;
}
public boolean isPretty() {
return pretty;
}
}
}
HealthController.java (pretty is an optional request parameter):
#RequestMapping(value = {"/", "/health"},
produces = APPLICATION_JSON_VALUE)
public ResponseEntity<?> health(#RequestParam Optional<String> pretty) {
return new ResponseEntity<>(
new CustomJsonResponseMapper.PrettyFormattedBody(healthResult(), pretty.isPresent()),
HttpStatus.OK);
}
Response example http://localhost:8080:
{"status":"OK","statusCode":200,"endpoints":["/aaa","/bbb","/ccc"]}
Response example http://localhost:8080?pretty:
{
"status": "OK",
"statusCode": 200,
"endpoints": [
"/aaa",
"/bbb",
"/ccc"
]
}
Another solution if Gson formatter is used (full pull request reference):
Spring Config (define 2 beans):
#Bean
public Gson gson() {
return new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
.disableHtmlEscaping()
.create();
}
/**
* #return same as {#link #gson()}, but with <code>{#link Gson#prettyPrinting} == true</code>, e.g. use indentation
*/
#Bean
public Gson prettyGson() {
return new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
.setPrettyPrinting()
.disableHtmlEscaping()
.create();
}
/**
* Custom JSON objects mapper: uses {#link #gson()} as a default JSON HTTP request/response mapper
* and {#link #prettyGson()} as mapper for pretty-printed JSON objects. See {#link PrettyGsonMessageConverter} for
* how pretty print is requested.
* <p>
* <b>Note:</b> {#link FieldNamingPolicy#IDENTITY} field mapping policy is important at least for
* {#link PaymentHandleResponse#getPayment()} method. See respective documentation for details.
*
* #return default HTTP request/response mapper, based on {#link #gson()} bean.
*/
#Bean
public GsonHttpMessageConverter gsonMessageConverter() {
return new PrettyGsonMessageConverter(gson(), prettyGson());
}
PrettyGsonMessageConverter.java:
/**
* Custom Gson response message converter to allow JSON pretty print, if requested.
* <p>
* The class extends default Spring {#link GsonHttpMessageConverter} adding {#link #prettyGson} mapper and processing
* {#link PrettyFormattedBody} instances.
*/
public class PrettyGsonMessageConverter extends GsonHttpMessageConverter {
/**
* JSON message converter with configured pretty print options, which is used when a response is expected to be
* pretty printed.
*/
private final Gson prettyGson;
/**
* #see GsonHttpMessageConverter#jsonPrefix
*/
private String jsonPrefix;
/**
* #param gson default (minified) JSON mapper. This value is set to {#code super.gson} property.
* #param prettyGson pretty configure JSON mapper, which is used if the body expected to be pretty printed
*/
public PrettyGsonMessageConverter(final Gson gson, final Gson prettyGson) {
super();
this.setGson(gson);
this.prettyGson = prettyGson;
}
/**
* Because base {#link GsonHttpMessageConverter#jsonPrefix} is private, but is used in overloaded
* {#link #writeInternal(Object, Type, HttpOutputMessage)} - we should copy this value.
*
* #see GsonHttpMessageConverter#setJsonPrefix(String)
*/
#Override
public void setJsonPrefix(String jsonPrefix) {
super.setJsonPrefix(jsonPrefix);
this.jsonPrefix = jsonPrefix;
}
/**
* Because base {#link GsonHttpMessageConverter#jsonPrefix} is private, but is used in overloaded
* {#link #writeInternal(Object, Type, HttpOutputMessage)} - we should copy this value.
*
* #see GsonHttpMessageConverter#setPrefixJson(boolean)
*/
#Override
public void setPrefixJson(boolean prefixJson) {
super.setPrefixJson(prefixJson);
this.jsonPrefix = (prefixJson ? ")]}', " : null);
}
/**
* Allow response JSON pretty print if {#code objectToWrite} is a {#link PrettyFormattedBody} instance with
* <code>{#link PrettyFormattedBody#isPretty() isPretty} == true</code>.
*
* #param objectToWrite if the value is {#link PrettyFormattedBody} instance with
* <code>{#link PrettyFormattedBody#isPretty() isPretty} == true</code> - use
* {#link #prettyGson} for output writing. Otherwise use base
* {#link GsonHttpMessageConverter#writeInternal(Object, Type, HttpOutputMessage)}
* #param type the type of object to write (may be {#code null})
* #param outputMessage the HTTP output message to write to
* #throws IOException in case of I/O errors
* #throws HttpMessageNotWritableException in case of conversion errors
*/
#Override
protected void writeInternal(#Nullable final Object objectToWrite,
#Nullable final Type type,
#Nonnull final HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
// based on: if objectToWrite is PrettyFormattedBody && isPretty == true => use custom formatter
// otherwise - use the default base GsonHttpMessageConverter#writeInternal(Object, Type, HttpOutputMessage)
Optional<PrettyFormattedBody> prettyFormatted = Optional.ofNullable(objectToWrite)
.filter(o -> o instanceof PrettyFormattedBody)
.map(o -> (PrettyFormattedBody) objectToWrite);
boolean pretty = prettyFormatted.map(PrettyFormattedBody::isPretty).orElse(false);
Object realObject = prettyFormatted.map(PrettyFormattedBody::getBody).orElse(objectToWrite);
if (pretty) {
// this is basically full copy of super.writeInternal(), but with custom (pretty) gson mapper
Charset charset = getCharset(outputMessage.getHeaders());
OutputStreamWriter writer = new OutputStreamWriter(outputMessage.getBody(), charset);
try {
if (this.jsonPrefix != null) {
writer.append(this.jsonPrefix);
}
if (type != null) {
this.prettyGson.toJson(realObject, type, writer);
} else {
this.prettyGson.toJson(realObject, writer);
}
writer.close();
} catch (JsonIOException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
}
} else {
// use default writer if isPretty property is not specified
super.writeInternal(realObject, type, outputMessage);
}
}
/**
* To ensure the message converter supports {#link PrettyFormattedBody} instances
*
* #param clazz response body class
* #return <b>true</b> if the {#code clazz} is {#link PrettyFormattedBody} or {#code super.supports(clazz) == true}
*/
#Override
protected boolean supports(Class<?> clazz) {
return PrettyFormattedBody.class.equals(clazz) || super.supports(clazz);
}
/**
* Just a copy-paste of {#link GsonHttpMessageConverter#getCharset(HttpHeaders)} because it is private, but used in
* {#link #writeInternal(Object, Type, HttpOutputMessage)}
*
* #param headers output message HTTP headers
* #return a charset from the {#code headers} content type or {#link GsonHttpMessageConverter#DEFAULT_CHARSET}
* otherwise.
*/
private Charset getCharset(HttpHeaders headers) {
if (headers == null || headers.getContentType() == null || headers.getContentType().getCharset() == null) {
return DEFAULT_CHARSET;
}
return headers.getContentType().getCharset();
}
}
PrettyFormattedBody.java:
public final class PrettyFormattedBody {
private final Object body;
private final boolean pretty;
private PrettyFormattedBody(#Nonnull final Object body, final boolean pretty) {
this.body = body;
this.pretty = pretty;
}
public Object getBody() {
return body;
}
public boolean isPretty() {
return pretty;
}
public static PrettyFormattedBody of(#Nonnull final Object body, final boolean pretty) {
return new PrettyFormattedBody(body, pretty);
}
}
and finally - the controller itself:
#RequestMapping(
value = {"/health", "/"},
produces = APPLICATION_JSON_VALUE)
public ResponseEntity<?> checkHealth(#RequestParam(required = false) String pretty,
#Autowired ApplicationInfo applicationInfo) {
Map<String, Object> tenantResponse = new HashMap<>();
tenantResponse.put(APP_INFO_KEY, applicationInfo);
return new ResponseEntity<>(PrettyFormattedBody.of(tenantResponse, pretty != null),
HttpStatus.OK);
}
I'm trying to obtain POJO instances using Gson and Retrofit2.
A typical JSON response looks like this.
My issue is with the Infobox field. In some cases, (like this) the field would be an object of the following type and an empty string otherwise.
class Infobox {
public List<Content> content = new ArrayList<>();
public List<Metum> meta;
}
class Content {
public String dataType;
public String value;
public String label;
public Integer wikiOrder;
}
class Metum {
public String dataType;
public String value;
public String label;
}
I tried writing a TypeAdapter as below
class InfoboxAdapter extends TypeAdapter<Infobox> {
final Gson embedded = new Gson();
#Override
public void write(JsonWriter out, Infobox infobox) throws IOException {
if (infobox == null) {
out.nullValue();
return;
}
out.beginObject();
out.name("content");
embedded.toJson(embedded.toJsonTree(infobox.content), out);
out.name("meta");
embedded.toJson(embedded.toJsonTree(infobox.meta), out);
out.endObject();
}
#Override
public Infobox read(JsonReader in) throws IOException {
if ("".equals(in.peek())) {
return null;
}
return embedded.fromJson(in, Infobox.class);
}
But it fails with java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING
The more confusing fact is that the field meta in the response, which is also an object, will in some cases have the value as null (and not an empty string like infobox)
I'd prefer to be able to do it using Gson as I've used it for everything else and I don't want to add another dependency
Hi Please go to :http://www.jsonschema2pojo.org/
paste your code. this sites automatically create your all related classes.
if issue please have a look at this link.
my drive link
I ended up using a JsonDeserializer. Google recommends:
New applications should prefer TypeAdapter, whose streaming API is more efficient than this interface's tree API.
But I didn't notice any performance impact for my use. I might someday rewrite this to use a TypeAdapter, but this works for me in till then
class InfoboxDeserialiser implements JsonDeserializer<Infobox> {
#Override
public Infobox deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
try {
if (json.isJsonNull() || json.isJsonPrimitive()) {
return null;
}
JsonObject jsonObject = json.getAsJsonObject();
Infobox infobox = new Infobox();
JsonArray jsonContent = jsonObject.get("content").getAsJsonArray();
JsonArray jsonMeta = jsonObject.get("meta").getAsJsonArray();
infobox.content = new Content[jsonContent.size()];
for (int i = 0; i < jsonContent.size(); i++) {
infobox.content[i] = context.deserialize(jsonContent.get(i), Content.class);
}
infobox.meta = new Metum[jsonMeta.size()];
for (int i = 0; i < jsonMeta.size(); i++) {
infobox.meta[i] = context.deserialize(jsonContent.get(i), Metum.class);
}
return infobox;
} catch (Exception e) {
Timber.e(e, "Failed to deserialise the infobox");
return null;
}
}
}
Where the classes are as follows
class Metum {
public String dataType;
public String value;
public String label;
}
class Content {
public String dataType;
public String value;
public String label;
public Integer wikiOrder;
}
I register this deserializer while creating the service object
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Infobox.class, new InfoboxDeserialiser());
GsonConverterFactory converterFactory = GsonConverterFactory.create(gsonBuilder.create());
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl("https://api.duckduckgo.com/")
.addConverterFactory(converterFactory);
I am trying to implement a universal method which serializes the given object to JSON, but only those properties which are passed in a collection. If possible I want to get this functionality without specifying #JsonFilter on the class. For this I am trying to use FilterExceptFilter from Jackson 2.4.1. Dependencies:
jackson-core-2.4.1.jar
jackson-databind-2.4.1.jar
jackson-annotations-2.4.0.jar
Here is what I have at the moment:
public static String serializeOnlyGivenFields(Object o,
Collection<String> fields) throws JsonProcessingException {
if ((fields == null) || fields.isEmpty()) return null;
Set<String> properties = new HashSet<String>(fields);
SimpleBeanPropertyFilter filter =
new SimpleBeanPropertyFilter.FilterExceptFilter(properties);
SimpleFilterProvider fProvider = new SimpleFilterProvider();
fProvider.addFilter("fieldFilter", filter);
fProvider.setDefaultFilter(filter);
ObjectMapper mapper = new ObjectMapper();
mapper.setFilters(fProvider);
String json = mapper.writeValueAsString(o);
return json;
}
However, the filter is never applied. It always serializes all properties.
Set<String> fields = new HashSet<String>(); fields.add("name");
String json = Serializer.serializeOnlyGivenFields(e, fields);
System.out.println(json);
{"name":"Test entity","description":"Test description"}
I have also tried to register the FilterProvider on the ObjectWriter, but same result:
String json = mapper.writer(fProvider).writeValueAsString(o);
What am I missing? Is there a nice way to achieve this with Jackson?
Based on http://www.cowtowncoder.com/blog/archives/2011/09/entry_461.html an alternate way to set up the filter is setting up a class that extends JacksonAnnotationIntrospector and overrides findFilterId. You can then specify to find your filter in the findFilterId. This could be made to be as robust if you want based on some other map or algorithm. Below is sample code. Not sure if the performance is better than the solution above but it seems to be simpler and probably more easily extensible. I was doing this for serializing CSV using Jackson. Any feedback is welcome!
public class JSON {
private static String FILTER_NAME = "fieldFilter";
public static String serializeOnlyGivenFields(Object o,
Collection<String> fields) throws JsonProcessingException {
if ((fields == null) || fields.isEmpty()) fields = new HashSet<String>();
Set<String> properties = new HashSet<String>(fields);
SimpleBeanPropertyFilter filter =
new SimpleBeanPropertyFilter.FilterExceptFilter(properties);
SimpleFilterProvider fProvider = new SimpleFilterProvider();
fProvider.addFilter(FILTER_NAME, filter);
ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector( new AnnotationIntrospector() );
String json = mapper.writer(fProvider).writeValueAsString(o);
return json;
}
private static class AnnotationIntrospector extends JacksonAnnotationIntrospector {
#Override
public Object findFilterId(Annotated a) {
return FILTER_NAME;
}
}
}
One additional thing is that you have to indicate Java classes for which filter is to be used by #JsonFilter annotation:
#JsonFilter("fieldFilter")
public class MyType { }
and then it should apply.
I have found a solution based on Jackson: How to add custom property to the JSON without modifying the POJO. I override BeanSerializer#serializeFields to always use BeanSerializer#serializeFieldsFiltered instead. This way the filter is always applied.
Performance-wise not a very good solution, since an ObjectMapper has to be constructed at every method call. Feel free to post improvements or suggestions!
Module implementation:
public class FilteredModule extends SimpleModule {
private static final long serialVersionUID = 1L;
#Override
public void setupModule(SetupContext context) {
super.setupModule(context);
context.addBeanSerializerModifier(new BeanSerializerModifier() {
#Override
public JsonSerializer<?> modifySerializer(
SerializationConfig config,
BeanDescription beanDesc,
JsonSerializer<?> serializer) {
if (serializer instanceof BeanSerializerBase) {
return new FilteredBeanSerializer(
(BeanSerializerBase) serializer);
}
return serializer;
}
});
}
private class FilteredBeanSerializer extends BeanSerializer {
public FilteredBeanSerializer(BeanSerializerBase source) {
super(source);
}
#Override
protected void serializeFields(Object arg0, JsonGenerator arg1,
SerializerProvider arg2) throws IOException,
JsonGenerationException {
super.serializeFieldsFiltered(arg0, arg1, arg2);
}
}
}
API method:
public static String serializeOnlyGivenFields(Object o,
Collection<String> fields) throws JsonProcessingException {
if ((fields == null) || fields.isEmpty()) fields = new HashSet<String>();
Set<String> properties = new HashSet<String>(fields);
SimpleBeanPropertyFilter filter =
new SimpleBeanPropertyFilter.FilterExceptFilter(properties);
SimpleFilterProvider fProvider = new SimpleFilterProvider();
fProvider.addFilter("fieldFilter", filter);
fProvider.setDefaultFilter(filter);
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new FilteredModule());
String json = mapper.writer(fProvider).writeValueAsString(o);
return json;
}
Example
Entity e = new Entity("Test entity", "Test description");
Set<String> fields = new HashSet<String>(); fields.add("name");
String json = JSON.serializeOnlyGivenFields(e, fields);
System.out.println(json);
{"name":"Test entity"}
Benchmark: 1000 iterations on the same object
serializeOnlyGivenFields: 536 ms
serialize (reuses ObjectMapper): 23 ms
public class RESTDataServiceClient{
private Client client;
private String dataServiceUri;
private String dataServiceResource;
private CustomData customData;
public RESTDataServiceClient(String dataServiceUri, String dataServiceResource, Client client){
this.client = client;
this.dataServiceUri = dataServiceUri;
this.dataServiceResource = dataServiceResource;
}
#Override
public CustomData getCustomData() {
WebTarget dataServiceTarget = client.target(dataServiceUri).path(dataServiceResource);
Invocation.Builder invocationBuilder = dataServiceTarget.request(MediaType.APPLICATION_JSON_TYPE);
Response response = invocationBuilder.get();
myCustomData = response.readEntity(CustomData.class);
return myCustomData;
}
}
CustomData.java
public class CustomData{
private TLongObjectMap<Map<String, TIntIntMap>> data;
public CustomData() {
this.data = new TLongObjectHashMap<>();
}
//getter and setter
}
sample json content
{"50000":{"testString":{"1":10}},"50001":{"testString1":{"2":11}} }
I am trying to get data from a data service which is going to return data in a JSON format. I am trying to write a client to read that JSON into a custom object. The CustomData contains a nested trove map datastructure. we wrote a custom serializer for that and the server part works fine. I am unable to get the rest client read the data into an object, but reading into string works. I tried above pasted code with the sample data and i get the error below.
javax.ws.rs.ProcessingException: Error reading entity from input stream.
at org.glassfish.jersey.message.internal.InboundMessageContext.readEntity(InboundMessageContext.java:866)
at org.glassfish.jersey.message.internal.InboundMessageContext.readEntity(InboundMessageContext.java:783)
at org.glassfish.jersey.client.ClientResponse.readEntity(ClientResponse.java:326)
at org.glassfish.jersey.client.InboundJaxrsResponse$1.call(InboundJaxrsResponse.java:111)
at org.glassfish.jersey.internal.Errors.process(Errors.java:315)
at org.glassfish.jersey.internal.Errors.process(Errors.java:297)
at org.glassfish.jersey.internal.Errors.process(Errors.java:228)
at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:399)
at org.glassfish.jersey.client.InboundJaxrsResponse.readEntity(InboundJaxrsResponse.java:108)
at com.sample.data.RESTDataServiceClient.getCustomData(RESTDataServiceClient.java:42)
Caused by: com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "50000" (class com.sample.data.CustomData), not marked as ignorable (0 known properties: ])
at [Source: org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$UnCloseableInputStream#2cb89281; line: 1, column: 14] (through reference chain: com.sample.data.CustomData["50000"])
at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:51)
at com.fasterxml.jackson.databind.DeserializationContext.reportUnknownProperty(DeserializationContext.java:671)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:773)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1297)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownVanilla(BeanDeserializerBase.java:1275)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:247)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:118)
at com.fasterxml.jackson.databind.ObjectReader._bind(ObjectReader.java:1233)
at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:677)
at com.fasterxml.jackson.jaxrs.base.ProviderBase.readFrom(ProviderBase.java:777)
at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$TerminalReaderInterceptor.invokeReadFrom(ReaderInterceptorExecutor.java:264)
at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$TerminalReaderInterceptor.aroundReadFrom(ReaderInterceptorExecutor.java:234)
at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor.proceed(ReaderInterceptorExecutor.java:154)
at org.glassfish.jersey.message.internal.MessageBodyFactory.readFrom(MessageBodyFactory.java:1124)
at org.glassfish.jersey.message.internal.InboundMessageContext.readEntity(InboundMessageContext.java:851)
... 38 more
TLongObjectMap is not deserializable out of the box, so how you made a custom serializer you also need to implement a custom deserializer. You can package these up nicely in a module and add it to your ObjectMapper.
It looks like there is a Trove module in development right now, which you can download and add to your ObjectMapper the same as the example below. The TIntObjectMapDeserializer implementation in that link is much more robust then my solution, so I would recommend using that class in your project if possible.
If you want to try and write it yourself, here's a starting point that properly deserializes your provided example:
public class FakeTest {
#Test
public void test() throws Exception {
ObjectMapper om = new ObjectMapper();
om.registerModule(new CustomModule());
String s = "{\"50000\":{\"testString\":{\"1\":10}},\"50001\":{\"testString1\":{\"2\":11}} }";
CustomData cd = om.readValue(s, CustomData.class);
System.out.println(cd.getData());
}
public static class CustomData {
private TLongObjectMap<Map<String, TIntIntMap>> data;
public CustomData() {
this.data = new TLongObjectHashMap<>();
}
public TLongObjectMap<Map<String, TIntIntMap>> getData() { return data; }
public void setData(TLongObjectMap<Map<String, TIntIntMap>> data) { this.data = data; }
}
public static class CustomModule extends SimpleModule {
public CustomModule() {
addSerializer(CustomData.class, new CustomSerializer());
addDeserializer(CustomData.class, new CustomDeserializer());
}
public static class CustomSerializer extends JsonSerializer<CustomData> {
#Override
public void serialize(CustomData value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
// add custom serializer here
}
}
public static class CustomDeserializer extends JsonDeserializer<CustomData> {
#Override
public CustomData deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
TLongObjectMap<Map<String, TIntIntMap>> data = new TLongObjectHashMap<>();
ObjectNode node = jsonParser.getCodec().readTree(jsonParser);
Iterator<Map.Entry<String,JsonNode>> fields = node.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> entry = fields.next();
ObjectNode value = (ObjectNode) entry.getValue();
Map.Entry<String, JsonNode> innerField = value.fields().next();
ObjectNode innerNode = (ObjectNode) innerField.getValue();
Map.Entry<String, JsonNode> innerInnerField = innerNode.fields().next();
TIntIntMap intMap = new TIntIntHashMap();
intMap.put(Integer.parseInt(innerInnerField.getKey()), innerInnerField.getValue().asInt());
Map<String, TIntIntMap> innerMap = Collections.singletonMap(innerField.getKey(), intMap);
data.put(Long.parseLong(entry.getKey()), innerMap);
}
CustomData customData = new CustomData();
customData.setData(data);
return customData;
}
}
}
}