how to globally define the naming convention with Jackson - java

I'm using Jackson with Spring. I have a few methods like this:
#RequestMapping(value = "/myURL", method = RequestMethod.GET)
public #ResponseBody Foo getFoo() {
// get foo
return foo;
}
The class Foo that's serialized is quite big and has many members. The serialization is ok, using annotation or custom serializer.
The only thing I can't figure out is how to define the naming convention. I would like to use snake_case for all the serializations.
So how do I define globally the naming convention for the serialization?
If it's not possible, then a local solution will have to do then.

Not sure how to do this globally but here's a way to do it at the JSON object level and not per each individual property:
#JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class Foo {
private String myBeanName;
//...
}
would yield json:
{
"my_bean_name": "Sth"
//...
}

Actually, there was a really simple answer:
#Bean
public Jackson2ObjectMapperBuilder jacksonBuilder() {
Jackson2ObjectMapperBuilder b = new Jackson2ObjectMapperBuilder();
b.propertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
return b;
}
I added it in my main like so:
#SpringBootApplication
public class Application {
public static void main(String [] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public Jackson2ObjectMapperBuilder jacksonBuilder() {
Jackson2ObjectMapperBuilder b = new Jackson2ObjectMapperBuilder();
b.propertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
return b;
}
}

The mapper has a setter for PropertyNamingStrategy (Method for setting custom property naming strategy to use.)
Look how it works in the tes example:
#Test
public void namingStrategy() throws Exception {
final ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(new PropertyNamingStrategy.PropertyNamingStrategyBase() {
#Override
public String translate(String s) {
return s.toUpperCase();
}
});
final String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(new SomePojo("uuid_1", "user_1", "Bruce", "W.", 51));
System.out.println(json);
}
public static class SomePojo {
private String someIdAttachedToIt;
private String username;
private String fistName;
private String lastName;
private int age;
public SomePojo(String someIdAttachedToIt, String username, String fistName, String lastName, int age) {
this.someIdAttachedToIt = someIdAttachedToIt;
this.username = username;
this.fistName = fistName;
this.lastName = lastName;
this.age = age;
}
public String getSomeIdAttachedToIt() {
return someIdAttachedToIt;
}
public String getUsername() {
return username;
}
public String getFistName() {
return fistName;
}
public String getLastName() {
return lastName;
}
public int getAge() {
return age;
}
}
Output:
{
"SOMEIDATTACHEDTOIT" : "uuid_1",
"USERNAME" : "user_1",
"FISTNAME" : "Bruce",
"LASTNAME" : "W.",
"AGE" : 51
}
Provided strategies (I use LOWERCASE for the examples)
PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES
PropertyNamingStrategy.SNAKE_CASE
To add your strategy globally in Spring, you can do it at least in 2 ways:
with a mapper module declared as a bean containing the naming strategy
with a custom object mapper configured as you want
Short version:
#Configuration
public static class Config {
#Bean
public Module module() {
return new SimpleModule() {
#Override
protected SimpleModule setNamingStrategy(PropertyNamingStrategy naming) {
super.setNamingStrategy(new PropertyNamingStrategy.PropertyNamingStrategyBase() {
#Override
public String translate(String propertyName) {
// example: "propertyName" -> "PROPERTYNAME"
return propertyName.toUpperCase();
}
});
return this;
}
};
}
}
Long version:
To declare the bean for the jackson module:
// config auto scan by spring
#Configuration
public static class ConfigurationClass {
// declare the module as a bean
#Bean
public Module myJsonModule() {
return new MySimpleModule();
}
}
// jackson mapper module to customize mapping
private static class MySimpleModule extends SimpleModule {
#Override
protected SimpleModule setNamingStrategy(PropertyNamingStrategy naming) {
return super.setNamingStrategy(new MyNameStrategy());
}
}
// your naming strategy
private static class MyNameStrategy extends PropertyNamingStrategy.PropertyNamingStrategyBase {
#Override
public String translate(String propertyName) {
return propertyName.toUpperCase();
}
}
You can declare the bean in xml as well.
It won't override #JsonProperty that define the prop name explicitly.

Related

I Injected the service Interface in Resource but not able to figure out how to configure Resource Method Calling

enter image description hereThis is the code I wrote to start the Resource. Not able to register the Resouce Class. Used Mongo database and the basics of #Injected
#JsonIgnoreProperties(ignoreUnknown = true)
public class Student extends BaseModel {
private String firstName;
private String lastName;
private String marks;
private long dob;
#Email
private String email;
public Student() {
}
public Student(String firstName, String lastName, String marks, long dob, #Email String email) {
this.firstName = firstName;
this.lastName = lastName;
this.marks = marks;
this.dob = dob;
this.email = email;
}
//Getter Setter method
}
//Base Repository Interface
public interface BaseRepository<T extends BaseModel> extends GenericRepository<T> {
T save (T model);
List<T> getAll();
T getById(String Id);
void deleteById(String Id);
T updateById(String Id, T model);
}
Abstract Repository Method to perform the Basics method of CRUD Operation
public BaseRepositoryImpl(MongoDb mongoManager, Class<T> clazz) throws Exception{
this.mongoManager = mongoManager;
collectionName = clazz.getAnnotation(CollectionName.class);
collection = mongoManager.getMongoCollection(collectionName.name());
entityClass = clazz;
}
#Override
public T save(T model) {
if(model == null){
throw new RuntimeException("No data Found");
}
Object id = collection.save(model).getUpsertedId();
if(id != null && id instanceof ObjectId){
model.setId(((ObjectId) id).toStringMongod());
}
return model;
}
}
//service
public interface StudentService {
Student save(Student student);
}
//Resource
WebService
#Path("/student")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
#HK2Managed
#Timed
public class StudentResource {
#Inject
StudentService studentService;
/* #Inject
public StudentResource(StudentService studentService) {
this.studentService = studentService;
}*/
#Path("/getName")
#GET
#Produces(MediaType.APPLICATION_JSON)
public String getName(){
return "Puru";
}
}
Configuration of the Application to Register the Uri and port of the Main Class
private String template;
// private StudentService studentService;
private String defaultName = "Stranger";
public MongoConfiguration getMongoConfiguration() {
return mongoConfiguration;
}
public void setMongoConfiguration(MongoConfiguration mongoConfiguration) {
this.mongoConfiguration = mongoConfiguration;
}
#Valid
#JsonProperty("mongoserver")
public MongoConfiguration mongoConfiguration;
}
//Application Connector to bind the Class in interface of the Respected Class
public class ApplicationConnector extends AbstractModule /*AbstractBinder*/ {
private final ConfigurationContext context;
public ApplicationConnector(ConfigurationContext context) {
this.context = context;
}
#PostConstruct
#Override
protected void configure() {
bind(StudentService.class).to(StudentServiceImpl.class);
bind(StudentRepository.class).to(StudentRepositoryImpl.class);
}
// applicaytion Connector Class
public class App extends Application<AppConfiguration> {
GuiceBundle<AppConfiguration> guiceBundle = null;
private JAXWSBundle jaxwsBundle = new JAXWSBundle();
public static void main(final String[] args) throws Exception {
new App().run(args);
}
private JAXWSBundle<Object> jaxWsBundle = new JAXWSBundle<>("/api");
#Override
public String getName() {
return "student-DropWizard-demo";
}
#Override
public void initialize(final Bootstrap<AppConfiguration> bootstrap) {
// TODO: application initializatio
// bootstrap.getObjectMapper().enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);
// bootstrap.addBundle(GuiceBundle.<AppConfiguration>builder()
// .enableAutoConfig("package.to.scan")
// .searchCommands(true)
// .build()
// );
/* GuiceBundle.Builder builder = GuiceBundle.builder() .noDefaultInstallers()
.enableAutoConfig("com.dzone");
guiceBundle = builder.build();*/
// bootstrap.addBundle(GuiceBundle.builder().enableAutoConfig("com.dzone")
// .build());
// Module[] modules = autoDiscoverModules();
bootstrap.getObjectMapper().registerSubtypes(DefaultServerFactory.class);
//bootstrap.getObjectMapper().registerModule(new FCSerializerModule());
bootstrap.getObjectMapper().enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);
bootstrap.addBundle(jaxwsBundle);
GuiceBundle.Builder builder = GuiceBundle.builder()
// .modules(modules)
.noDefaultInstallers()
.installers(new Class[]{LifeCycleInstaller.class,
ManagedInstaller.class,
JerseyFeatureInstaller.class, ResourceInstaller.class,
JerseyProviderInstaller.class,
EagerSingletonInstaller.class,
HealthCheckInstaller.class,
TaskInstaller.class,
PluginInstaller.class
})
.enableAutoConfig(ApplicationConnector.class.getPackage().getName());
postInitialize(bootstrap, builder);
guiceBundle = builder.build();
bootstrap.addBundle(guiceBundle);
}
#Override
public void run(final AppConfiguration configuration,
final Environment environment) throws Exception {
// TODO: implement application
// FilterRegistration.Dynamic dFilter = environment.servlets().addFilter("student", CrossOriginFilter.class);
// AbstractServerFactory sf = (AbstractServerFactory) configuration.getServerFactory();
// Endpoint e = jaxWsBundle.publishEndpoint(
// new EndpointBuilder("student", new StudentResource()));
// environment.jersey().register(new StudentResource());
//environment.jersey().register(new StudentServiceImpl());
//environment.jersey().packages("service");
// environment.jersey().disable();
// environment.servlets().addServlet(StudentResource.class).addMapping("/student");
environment.getJerseyServletContainer().getServletInfo();
//environment.servlets().setBaseResource("");
///environment.servlets().addServlet("StudentResource",StudentResource.class);
//environment.jersey().register(new ResourceInstaller());
postRun(configuration,environment);
}
protected void postRun(final AppConfiguration configuration, final Environment environment) throws Exception {
// Sub-classes should
}
protected void postInitialize(Bootstrap<AppConfiguration> bootstrapm, GuiceBundle.Builder guiceBuilder) {
// Sub-classes should
}
/*public Module[] autoDiscoverModules() {
Reflections reflections =
new Reflections(
new ConfigurationBuilder()
.forPackages(
"com.dzone"));
Set<Class<? extends AbstractModule>> classes = reflections.getSubTypesOf(AbstractModule.class);
List<Module> discoveredModules = new ArrayList<>();
for (Class clazz : classes) {
try {
AbstractModule module = (AbstractModule) clazz.newInstance();
discoveredModules.add(module);
} catch (Exception e) {
e.printStackTrace();
}
}
return discoveredModules.toArray(new Module[]{});
}
}
This is the output of the Code. It's not able to register the the resource of the application
WARN [2019-09-09 05:32:31,707] io.dropwizard.setup.AdminEnvironment:
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
! THIS APPLICATION HAS NO HEALTHCHECKS. THIS MEANS YOU WILL NEVER KNOW !
! IF IT DIES IN PRODUCTION, WHICH MEANS YOU WILL NEVER KNOW IF YOU'RE !
! LETTING YOUR USERS DOWN. YOU SHOULD ADD A HEALTHCHECK FOR EACH OF YOUR !
! APPLICATION'S DEPENDENCIES WHICH FULLY (BUT LIGHTLY) TESTS IT. !
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
INFO [2019-09-09 05:32:31,711] org.eclipse.jetty.server.handler.ContextHandler: Started i.d.j.MutableServletContextHandler#615db358{/,null,AVAILABLE}
INFO [2019-09-09 05:32:31,718] org.eclipse.jetty.server.AbstractConnector: Started application#f9cab00{HTTP/1.1,[http/1.1]}{0.0.0.0:8099}
INFO [2019-09-09 05:32:31,719] org.eclipse.jetty.server.AbstractConnector: Started admin#10272bbb{HTTP/1.1,[http/1.1]}{0.0.0.0:8091}
INFO [2019-09-09 05:32:31,719] org.eclipse.jetty.server.Server: Started #5573ms
INFO [2019-09-09 05:32:31,719] com.roskart.dropwizard.jaxws.JAXWSEnvironment: No JAX-WS service endpoints were registered.
In the example above you declare Resource as HK managed:
#HK2Managed
#Timed
public class StudentResource {
Which means that HK2 (jersey's DI) will instantiate object and not guice.
But your service is declared in guice module:
public class ApplicationConnector extends AbstractModule /*AbstractBinder*/ {
private final ConfigurationContext context;
public ApplicationConnector(ConfigurationContext context) {
this.context = context;
}
#PostConstruct
#Override
protected void configure() {
bind(StudentService.class).to(StudentServiceImpl.class);
bind(StudentRepository.class).to(StudentRepositoryImpl.class);
}
So it's normal that your service can't inject StudentService as HK2 is not aware of guice dependencies.
This could be fixed by activating HK2-guice bridge so HK could use guice bindings:
Add dependency: org.glassfish.hk2:guice-bridge:2.5.0-b32 (version must match HK2 version, used by dropwizard)
Enable option: .option(GuiceyOptions.UseHkBridge, true)
In your github project (student-gradle) you have an opposite situation:
Resource is guice managed (guice creates resource instance)
#Path("/student")
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
#Timed
public class StudentResource {
And service is declared in HK:
public class ApplicationConnector extends /*AbstractModule*/ AbstractBinder {
#PostConstruct
#Override
protected void configure() {
bind(StudentService.class).to(StudentServiceImpl.class);
bind(StudentRepository.class).to(StudentRepositoryImpl.class);
}
}
(but this module is actually never registered)
My suggestion is to "live" in guice context: don't use #HKManaged (it was added only for edge cases) and use guice modules for services declaration. This way guice will be responsible for all DI related to your code.

Custom attributes cannot inject in spring boot

I just want to implement custom attributes inject in spring boot.But it does
not work well.
First, I create a annotation like below
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
public #interface ConfigValue {
String name();
}
Second,I create a config class with customize annotation like below
#Configuration
public class MysqlConf {
#Value("${spring.datasource.driverClassName}")
private String className;
#Value("${spring.datasource.url}")
private String jdbcUrl;
#Value("${spring.datasource.username}")
private String username;
#ConfigValue(name = "spring.datasource.password")
private String password;
#Bean
public DataSource druidDataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(className);
druidDataSource.setUsername(username);
druidDataSource.setPassword(password);
druidDataSource.setUrl(jdbcUrl);
druidDataSource.setMaxActive(20);
druidDataSource.setInitialSize(1);
druidDataSource.setMinIdle(1);
druidDataSource.setMaxWait(60000);
druidDataSource.setTimeBetweenEvictionRunsMillis(60000);
druidDataSource.setMinEvictableIdleTimeMillis(300000);
druidDataSource.setValidationQuery("select 1");
druidDataSource.setTestWhileIdle(true);
druidDataSource.setTestOnBorrow(false);
druidDataSource.setTestOnReturn(false);
druidDataSource.setPoolPreparedStatements(true);
druidDataSource.setMaxPoolPreparedStatementPerConnectionSize(50);
return druidDataSource;
}
}
Then I set custom attributes like below
#Component
public class BeanPropertiesUtil implements
InitializingBean,BeanFactoryAware {
private static final Logger logger =
LoggerFactory.getLogger(BeanPropertiesUtil.class);
private BeanFactory beanFactory;
public static Map<String,Object> configMap = new HashMap<>();
#PostConstruct
public void init() throws Exception {
configMap.put("spring.datasource.password","123321");
configMap.put("orderNo","2018051929991");
}
#Override
public void afterPropertiesSet() throws Exception {
logger.info("config map : "+ JSON.toJSONString(configMap));
ApplicationContext applicationContext =
BeanNameUtil.getApplicationContext();
final String[] beanDefinitionNames =
applicationContext.getBeanDefinitionNames();
for(String beanName : beanDefinitionNames) {
Object bean = beanFactory.getBean(beanName);
final Field[] declaredFields = bean.getClass().getDeclaredFields();
if(declaredFields.length > 0) {
for(Field field : declaredFields) {
final ConfigValue configValue =
field.getAnnotation(ConfigValue.class);
if(configValue != null) {
field.setAccessible(true);
logger.info("field name :"+field.getName() +" field
value:"+configMap.get(configValue.name()));
field.set(bean,configMap.get(configValue.name()));
}
}
}
}
}
#Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
}
It cannot get password value, I also create an other class
#Service
public class MyAnnotationService {
#ConfigValue(name = "spring.datasource.password")
private String password;
public void deSth() {
System.err.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:"+password);
}
}
it works,
I do not know why MysqlConf can't get the value but MyAnnotationService can.Please help me,Thanks!
bean.getClass().getDeclaredFields();
not returning fields for #Configuration MysqlConf class, it works if you change MysqlConf to #Service

#Value with SpEL not able to get values from properties

I have the following code in a sample Spring Boot Application
#Configuration
#PropertySource("classpath:second.properties")
public class PropertyConfig {
#Value("#{guru.username}")
String user;
#Value("#{guru.password}")
String password;
#Value("#{guru.url}")
String url;
#Bean
FakeDataSource getFakeDataSource() {
FakeDataSource fk = new FakeDataSource();
fk.setName(user);
fk.setPassword(password);
fk.setUrl(url);
return fk;
}
#Bean
PropertySourcesPlaceholderConfigurer getPropertySourcesPlaceholderConfigurer() {
PropertySourcesPlaceholderConfigurer placeholderConfigurer= new PropertySourcesPlaceholderConfigurer();
//placeholderConfigurer.setLocation(new ClassPathResource("second.properties"));
return placeholderConfigurer;
}
}
And FakeDataSource is a simple pojo with the name, passowrd, url properties.
Then my main application
#SpringBootApplication
public class SpringGuru101DependencyInjectionApplication {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(SpringGuru101DependencyInjectionApplication.class, args);
// Step 2: Make Class
FakeDataSource fakeDataSource = ctx.getBean(FakeDataSource.class);
System.out.println(fakeDataSource.getName());
}
}
but the sout statement is printing null,
my second.properties file is present in my resources directory with following content
guru.username=Saurabh
guru.password=ido
guru.url=http://example.com
There are two places should be corrected:
(1) As I said in the comment of your question, you should replace the wellhead sign (#) to dollar sign ($) for reading values from your configuration file. For example: #Value("${guru.username}").
(2) You missed public static in front of the method getPropertySourcesPlaceholderConfigurer.
And this modified method should be looked like as follows:
#Bean
public static PropertySourcesPlaceholderConfigurer getPropertySourcesPlaceholderConfigurer() {
PropertySourcesPlaceholderConfigurer placeholderConfigurer= new PropertySourcesPlaceholderConfigurer();
//placeholderConfigurer.setLocation(new ClassPathResource("second.properties"));
return placeholderConfigurer;
}
If you are using Spring-boot, you can use another method for reading configuration which is Spring boot recommended. This will help you get rid of all the #Value notations, allowing spring inject properties without additional hints.
You can potentially do something like:
#ConfigurationProperties("foo")
public class FooProperties {
private boolean enabled;
private InetAddress remoteAddress;
private final Security security = new Security();
public boolean isEnabled() { ... }
public void setEnabled(boolean enabled) { ... }
public InetAddress getRemoteAddress() { ... }
public void setRemoteAddress(InetAddress remoteAddress) { ... }
public Security getSecurity() { ... }
public static class Security {
private String username;
private String password;
private List<String> roles = new ArrayList<>(Collections.singleton("USER"));
public String getUsername() { ... }
public void setUsername(String username) { ... }
public String getPassword() { ... }
public void setPassword(String password) { ... }
public List<String> getRoles() { ... }
public void setRoles(List<String> roles) { ... }
}
}
The POJO above defines the following properties:
foo.enabled, false by default
foo.remote-address, with a type that can be coerced from String
foo.security.username, with a nested "security" whose name is determined by the name of the property. In particular the return type is not used at all there and could have been SecurityProperties
foo.security.password
foo.security.roles, with a collection of String
More details: https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html

Spring 3 with ResponseBody ignoring #JsonTypeInfo

I can't make spring return a serialization of my object with the additional property defining the class.
My classes are:
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include= JsonTypeInfo.As.PROPERTY, property="ObjectType")
#JsonSubTypes({
#JsonSubTypes.Type(value=LiteStudy.class, name="LiteStudy")
})
public class Entity {
...
}
#JsonTypeName("LiteStudy")
#JsonSubTypes({
#JsonSubTypes.Type(value=Study.class, name="Study")
})
public class LiteStudy extends Entity {
...
}
#JsonTypeName("Study")
public class Study extends LiteStudy{
...
}
In my unit test, an Study instance is serialised properly, having the extra property for the class:
{"ObjectType":"Study",
...
}
Using for this a simple:
ObjectMapper mapper = new ObjectMapper();
mapper.readValue(studyJSON,study.getClass());
However, in my Spring Rest webservice module the study is serialized without the "ObjectType" property.
The controller looks like this (simplified):
#ResponseBody
public RestResponse<Study> getStudyById(#PathVariable("studyIdentifier") String studyIdentifier) throws DAOException {
return getStudyRestResponse(studyIdentifier);
}
EDIT: Adding RestResponse (Simplified)
public class RestResponse<Content> {
private Content content;
private String message;
private Exception err;
public Content getContent() {
return content;
}
public void setContent(Content content) {
this.content = content;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Exception getErr() {
return err;
}
public void setErr(Exception err) {
this.err = err;
}
Any idea why spring seems to be ignoring the #JsonType annotations?
Try to return only the object you need, do not wrap it in generic wrapper class. Your problem is related to Java Type erasure. See more info here
#ResponseBody
public #ResponseBody Study getStudyById(#PathVariable("studyIdentifier") String studyIdentifier) throws DAOException {
return studyIdentifier;
}

Jackson's #JsonView, #JsonFilter and Spring

Can one use the Jackson #JsonView and #JsonFilter annotations to modify the JSON returned by a Spring MVC controller, whilst using MappingJacksonHttpMessageConverterand Spring's #ResponseBody and #RequestBody annotations?
public class Product
{
private Integer id;
private Set<ProductDescription> descriptions;
private BigDecimal price;
...
}
public class ProductDescription
{
private Integer id;
private Language language;
private String name;
private String summary;
private String lifeStory;
...
}
When the client requests a collection of Products, I'd like to return a minimal version of each ProductDescription, perhaps just its ID. Then in a subsequent call the client can use this ID to ask for a full instance of ProductDescription with all properties present.
It would be ideal to be able to specify this on the Spring MVC controller methods, as the method invoked defines the context in which client was requesting the data.
This issue is solved!
Follow this
Add support for Jackson serialization views
Spring MVC now supports Jackon's serialization views for rendering
different subsets of the same POJO from different controller
methods (e.g. detailed page vs summary view).
Issue: SPR-7156
This is the SPR-7156.
Status: Resolved
Description
Jackson's JSONView annotation allows the developer to control which aspects of a method are serialiazed. With the current implementation, the Jackson view writer must be used but then the content type is not available. It would be better if as part of the RequestBody annotation, a JSONView could be specified.
Available on Spring ver >= 4.1
UPDATE
Follow this link. Explains with an example the #JsonView annotation.
Ultimately, we want to use notation similar to what StaxMan showed for JAX-RS. Unfortunately, Spring doesn't support this out of the box, so we have to do it ourselves.
This is my solution, it's not very pretty, but it does the job.
#JsonView(ViewId.class)
#RequestMapping(value="get", method=RequestMethod.GET) // Spring controller annotation
public Pojo getPojo(#RequestValue Long id)
{
return new Pojo(id);
}
public class JsonViewAwareJsonView extends MappingJacksonJsonView {
private ObjectMapper objectMapper = new ObjectMapper();
private boolean prefixJson = false;
private JsonEncoding encoding = JsonEncoding.UTF8;
#Override
public void setPrefixJson(boolean prefixJson) {
super.setPrefixJson(prefixJson);
this.prefixJson = prefixJson;
}
#Override
public void setEncoding(JsonEncoding encoding) {
super.setEncoding(encoding);
this.encoding = encoding;
}
#Override
public void setObjectMapper(ObjectMapper objectMapper) {
super.setObjectMapper(objectMapper);
this.objectMapper = objectMapper;
}
#Override
protected void renderMergedOutputModel(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
Class<?> jsonView = null;
if(model.containsKey("json.JsonView")){
Class<?>[] allJsonViews = (Class<?>[]) model.remove("json.JsonView");
if(allJsonViews.length == 1)
jsonView = allJsonViews[0];
}
Object value = filterModel(model);
JsonGenerator generator =
this.objectMapper.getJsonFactory().createJsonGenerator(response.getOutputStream(), this.encoding);
if (this.prefixJson) {
generator.writeRaw("{} && ");
}
if(jsonView != null){
SerializationConfig config = this.objectMapper.getSerializationConfig();
config = config.withView(jsonView);
this.objectMapper.writeValue(generator, value, config);
}
else
this.objectMapper.writeValue(generator, value);
}
}
public class JsonViewInterceptor extends HandlerInterceptorAdapter
{
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
JsonView jsonViewAnnotation = handlerMethod.getMethodAnnotation(JsonView.class);
if(jsonViewAnnotation != null)
modelAndView.addObject("json.JsonView", jsonViewAnnotation.value());
}
}
In spring-servlet.xml
<bean name="ViewResolver" class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<property name="mediaTypes">
<map>
<entry key="json" value="application/json" />
</map>
</property>
<property name="defaultContentType" value="application/json" />
<property name="defaultViews">
<list>
<bean class="com.mycompany.myproject.JsonViewAwareJsonView">
</bean>
</list>
</property>
</bean>
and
<mvc:interceptors>
<bean class="com.mycompany.myproject.JsonViewInterceptor" />
</mvc:interceptors>
I don't know how things work with Spring (sorry!), but Jackson 1.9 can use #JsonView annotation from JAX-RS methods, so you can do:
#JsonView(ViewId.class)
#GET // and other JAX-RS annotations
public Pojo resourceMethod()
{
return new Pojo();
}
and Jackson will use View identified by ViewId.class as the active view. Perhaps Spring has (or will have) similar capability? With JAX-RS this is handled by standard JacksonJaxrsProvider, for what that's worth.
Looking for the same answer I came up with an idea to wrap ResponseBody object with a view.
Piece of controller class:
#RequestMapping(value="/{id}", headers="Accept=application/json", method= RequestMethod.GET)
public #ResponseBody ResponseBodyWrapper getCompany(HttpServletResponse response, #PathVariable Long id){
ResponseBodyWrapper responseBody = new ResponseBodyWrapper(companyService.get(id),Views.Owner.class);
return responseBody;
}
public class ResponseBodyWrapper {
private Object object;
private Class<?> view;
public ResponseBodyWrapper(Object object, Class<?> view) {
this.object = object;
this.view = view;
}
public Object getObject() {
return object;
}
public void setObject(Object object) {
this.object = object;
}
#JsonIgnore
public Class<?> getView() {
return view;
}
#JsonIgnore
public void setView(Class<?> view) {
this.view = view;
}
}
Then I override writeInternal method form MappingJackson2HttpMessageConverter to check if object to serialize is instanceof wrapper, if so I serialize object with required view.
public class CustomMappingJackson2 extends MappingJackson2HttpMessageConverter {
private ObjectMapper objectMapper = new ObjectMapper();
private boolean prefixJson;
#Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
JsonGenerator jsonGenerator =
this.objectMapper.getJsonFactory().createJsonGenerator(outputMessage.getBody(), encoding);
try {
if (this.prefixJson) {
jsonGenerator.writeRaw("{} && ");
}
if(object instanceof ResponseBodyWrapper){
ResponseBodyWrapper responseBody = (ResponseBodyWrapper) object;
this.objectMapper.writerWithView(responseBody.getView()).writeValue(jsonGenerator, responseBody.getObject());
}else{
this.objectMapper.writeValue(jsonGenerator, object);
}
}
catch (IOException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
}
}
public void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
this.objectMapper = objectMapper;
super.setObjectMapper(objectMapper);
}
public ObjectMapper getObjectMapper() {
return this.objectMapper;
}
public void setPrefixJson(boolean prefixJson) {
this.prefixJson = prefixJson;
super.setPrefixJson(prefixJson);
}
}
The answer to this after a many head banging dead ends and nerd rage tantrums is....
so simple. In this use case we have a Customer bean with a complex object Address embedded within it and we want to prevent the serialization of a property name surburb and street in address, when the json serialization take place.
We do this by applying an annotation #JsonIgnoreProperties({"suburb"}) on the field address in the Customer class, the number of fields to be ignored is limitless. e.g i want to ingnore both suburb and street. I would annotate the address field with #JsonIgnoreProperties({"suburb", "street"})
By doing all this we can create HATEOAS type architecture.
Below is the full code
Customer.java
public class Customer {
private int id;
private String email;
private String name;
#JsonIgnoreProperties({"suburb", "street"})
private Address address;
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Address.java
public class Address {
private String street;
private String suburb;
private String Link link;
public Link getLink() {
return link;
}
public void setLink(Link link) {
this.link = link;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getSuburb() {
return suburb;
}
public void setSuburb(String suburb) {
this.suburb = suburb;
}
}
In addition to #user356083 I've made some modifications to make this example work when a #ResponseBody is returned. It's a bit of a hack using ThreadLocal but Spring doesn't seem to provide the necessary context to do this the nice way.
public class ViewThread {
private static final ThreadLocal<Class<?>[]> viewThread = new ThreadLocal<Class<?>[]>();
private static final Log log = LogFactory.getLog(SocialRequestUtils.class);
public static void setKey(Class<?>[] key){
viewThread.set(key);
}
public static Class<?>[] getKey(){
if(viewThread.get() == null)
log.error("Missing threadLocale variable");
return viewThread.get();
}
}
public class JsonViewInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
JsonView jsonViewAnnotation = handlerMethod
.getMethodAnnotation(JsonView.class);
if (jsonViewAnnotation != null)
ViewThread.setKey(jsonViewAnnotation.value());
return true;
}
}
public class MappingJackson2HttpMessageConverter extends
AbstractHttpMessageConverter<Object> {
#Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
JsonGenerator jsonGenerator =
this.objectMapper.getJsonFactory().createJsonGenerator(outputMessage.getBody(), encoding);
// This is a workaround for the fact JsonGenerators created by ObjectMapper#getJsonFactory
// do not have ObjectMapper serialization features applied.
// See https://github.com/FasterXML/jackson-databind/issues/12
if (objectMapper.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
jsonGenerator.useDefaultPrettyPrinter();
}
//A bit of a hack.
Class<?>[] jsonViews = ViewThread.getKey();
ObjectWriter writer = null;
if(jsonViews != null){
writer = this.objectMapper.writerWithView(jsonViews[0]);
}else{
writer = this.objectMapper.writer();
}
try {
if (this.prefixJson) {
jsonGenerator.writeRaw("{} && ");
}
writer.writeValue(jsonGenerator, object);
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
}
}
Even better, since 4.2.0.Release, you can do this simply as follows:
#RequestMapping(method = RequestMethod.POST, value = "/springjsonfilter")
public #ResponseBody MappingJacksonValue byJsonFilter(...) {
MappingJacksonValue jacksonValue = new MappingJacksonValue(responseObj);
jacksonValue.setFilters(customFilterObj);
return jacksonValue;
}
References:
1. https://jira.spring.io/browse/SPR-12586
2. http://wiki.fasterxml.com/JacksonFeatureJsonFilter

Categories

Resources