I created a mixin for my class. The mixin itself works fine, it's not the issue that most people have where they mix faterxml/codehaus annotations.
I tested it in a unit test, creating the ObjectMapper "by hand" while using the addMixIn method - it worked just fine.
I want to use that mixin to modify the response jsons returned from my REST endpoints.
I've tried to customize Spring Boot's ObjectMapper in many different ways:
BuilderCustomizer:
#Bean
public Jackson2ObjectMapperBuilderCustomizer addMixin(){
return new Jackson2ObjectMapperBuilderCustomizer() {
#Override
public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
jacksonObjectMapperBuilder.mixIn(MyClass.class, MyClassMixin.class);
}
};
}
Builder:
#Bean
Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {
return new Jackson2ObjectMapperBuilder().mixIn(MyClass.class, MyClassMixin.class);
}
Converter:
#Bean
public MappingJackson2HttpMessageConverter configureJackson(){
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(MyClass.class, MyClassMixin.class);
converter.setObjectMapper(mapper);
return converter;
}
ObjectMapper:
#Autowired(required = true)
public void configureJackon(ObjectMapper jsonMapper){
jsonMapper.addMixIn(MyClass.class, MyClassMixin.class);
}
None of these work.
As of Spring Boot 2.7, there is built-in support for mixins.
Adding the following annotation:
#JsonMixin(MyClass::class)
class MyClassMixin{
will register mixin in the auto-configured ObjectMapper.
This might depend on Spring Boot version but as per Customize the Jackson ObjectMapper defining a new Jackson2ObjectMapperBuilderCustomizer bean is sufficient
The context’s Jackson2ObjectMapperBuilder can be customized by one or more Jackson2ObjectMapperBuilderCustomizer beans. Such customizer beans can be ordered (Boot’s own customizer has an order of 0), letting additional customization be applied both before and after Boot’s customization.
I had tried the above and it did not work for me either. While debugging, I noticed that the ObjectMapper inside the message converter was null.
Referring to the post get registered message converters, I ended up replacing the default message converter for Jackson, allowing me to customize the object mapper to my needs:
#SpringBootApplication
#RestController
public class MixinTest {
public static void main(String[] args) {
SpringApplication.run(MixinTest.class, args);
}
static class Person {
private String title;
private String name;
private String nullField;
private LocalDate date;
Person(String title, String name) {
this.title = title;
this.name = name;
this.date = LocalDate.now();
}
// getters here...
}
// this will exclude nullField
#JsonInclude(JsonInclude.Include.NON_NULL)
interface PersonMixin {
#JsonProperty("fullName")
String getName();
}
#Bean
public Jackson2ObjectMapperBuilderCustomizer personCustomizer() {
return jacksonObjectMapperBuilder ->
jacksonObjectMapperBuilder.mixIn(Person.class, PersonMixin.class);
}
#Bean
public MappingJackson2HttpMessageConverter myMessageConverter(
// provided by Spring
RequestMappingHandlerAdapter reqAdapter,
Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
ObjectMapper mapper = jacksonObjectMapperBuilder
.featuresToEnable(SerializationFeature.INDENT_OUTPUT)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.modulesToInstall(new JavaTimeModule())
.build();
// **replace previous MappingJackson converter**
List<HttpMessageConverter<?>> converters =
reqAdapter.getMessageConverters();
converters.removeIf(httpMessageConverter ->
httpMessageConverter.getClass()
.equals(MappingJackson2HttpMessageConverter.class));
MappingJackson2HttpMessageConverter jackson = new
MappingJackson2HttpMessageConverter(mapper);
converters.add(jackson);
reqAdapter.setMessageConverters(converters);
return jackson;
}
#GetMapping("/test")
public Person get() {
return new Person("Mr", "Joe Bloggs");
}
}
Which outputs the following in the browser after hitting http://localhost:8080/test:
{
"title" : "Mr",
"date" : "2019-09-03",
"fullName" : "Joe Bloggs"
}
This way, I should be able to add as many customizers as necessary. I'm sure there's a better way to do this. It seems hacky to replace internals like this...
Related
These two are Service classes, which makes http calls to other services to retreive the json response. Just wanted to know if this is a proper way to use ResourceConverter which helps to convert json response string to POJO.
bIf you notice, these two seperate classes contains its own ResourceConverter Bean method. I cant create a single method which can be shared by all classes, because we need DummyResponse.class and TestResponse.class as the parameter. So, Is this way acceptable? Is it good practise to have multiple bean methods which return the same type, but in different classes?
#Service
public class TestClient(){
#Bean
private ResourceConverter getTestResponseConverter(){
ObjectMapper mapper = new ObjectMapper();
mapper.setDeserialization(DeserializationFeature.ALLOW_UNKNOWN_PROPERTIES);
ResourceConverter converter = new ResourceConverter(mapper,TestResponse.class);
return converter;
}
private TestResponse getTestResponse(){
//okhttp call to get the json, this is not actual syntax for call, not relevant in this context
String responseBodyString = okhttp.call(request);
JSONAPIDocument<TestResponse> testDocument = getTestResponseConverter().readDocument(responseBodyString, TestResponse.class);
return testDocument.get();
}
}
#Service
public class DummyClient(){
#Bean
private ResourceConverter getDummyResponseConverter(){
ObjectMapper mapper = new ObjectMapper();
mapper.setDeserialization(DeserializationFeature.ALLOW_UNKNOWN_PROPERTIES);
ResourceConverter converter = new ResourceConverter(mapper,DummyResponse.class);
return converter;
}
private Dummy getDummyResponse(){
//okhttp call to get the json, this is not actual syntax for call, not relevant in this context
String responseBodyString = okhttp.call(request);
JSONAPIDocument<DummyResponse> dummyDocument = getDummyResponseConverter().readDocument(responseBodyString, DummyResponse.class);
return testDocument.get();
}
}
You should/can use #Primary to annotate the one that will take precedence if there is an #Autowired ResourceConverter... and Spring can't tell the difference.
You can use #Qualifier to "name" each one and then the same annotation on the #Autowired bean to get the one you want.
So:
#Bean
private ResourceConverter testResponseConverter(){
...
}
and
#Bean
#Primary
private ResourceConverter dummyResponseConverter(){
...
}
You can do:
#Autowired ResourceConverter rc; // Gets the #Primary dummy one
#Autowired #Qualifier("testResponseConverter") ResourceConverter rc2; // Gets the corresponding testResponseConverter
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/annotation/Qualifier.html
I used to have a #RabbitListener that works fine like this:
#Component
public class AppointmentMessageListener {
#RabbitListener(queues = "${rabbitmq.immediateQueueName}")
public void receiveImmediateAppointmentMessage(AppointmentMessage appointmentMessage) {
// Do something
}
}
Now I want to have a different type of message on the same queue, and I tried it like the docs said:
#Component
#RabbitListener(queues = "${rabbitmq.immediateQueueName}")
public class AppointmentMessageListener {
#RabbitHandler
public void receiveImmediateAppointmentMessage(AppointmentMessage appointmentMessage) {
// Do something
}
#RabbitHandler
public void receiveImmediateAppointmentMessage(AppointmentDeleteMessage appointmentDeleteMessage) {
// Do something else
}
}
This doesn't work, and I get org.springframework.amqp.AmqpException: No method found for class java.util.LinkedHashMap.
The JSON looks like below, and the difference between the 2 types is only in the object structure. I don't control the structure of the message. Is there any way I can detect the correct type to use in my multimethod listener?
{
"key":"event-message-resource.immediate",
"creationTimestamp":1643804135376,
"object": {
// here is the object I am interested in
}
}
I use the following configuration besides the exchange, queue and routing key declarables.:
#Bean
public Jackson2JsonMessageConverter jsonMessageConverter() {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().build();
return new Jackson2JsonMessageConverter(objectMapper);
}
#Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer,
ConnectionFactory connectionFactory,
AuthenticationRabbitListenerAdvice rabbitListenerAdvice) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
configurer.configure(factory, connectionFactory);
factory.setAdviceChain(combineAdvice(factory.getAdviceChain(), rabbitListenerAdvice));
return factory;
}
The framework can't infer the target type when using #RabbitHandler because it uses the type to select the method. The JSON has to be converted before the method selection is performed.
See https://docs.spring.io/spring-amqp/docs/current/reference/html/#Jackson2JsonMessageConverter-from-message
You need to use something in the message to determine which type to create.
Based on the answer from the revered Gary Russell and his answer on his post here, I managed to get it to work by subclassing the ClassMapper and deciding on the type based on the routing key.
#Component
#RequiredArgsConstructor
public class CustomClassMapper extends DefaultJackson2JavaTypeMapper {
#Value("${rabbitmq.deleteRoutingKey}")
private final String deleteRoutingKey;
#NonNull
#Override
public Class<?> toClass(MessageProperties properties) {
String routingKey = properties.getReceivedRoutingKey();
if (deleteRoutingKey.equals(routingKey)) {
return AppointmentDeleteMessage.class;
}
return AppointmentMessage.class;
}
}
Then I set the classMapper in the messageConverter.
#Bean
public Jackson2JsonMessageConverter jsonMessageConverter(CustomClassMapper customClassMapper) {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().build();
Jackson2JsonMessageConverter messageConverter = new Jackson2JsonMessageConverter(objectMapper);
messageConverter.setClassMapper(customClassMapper);
return messageConverter;
}
I've made a very simple adjustment to the ObjectMapper configuration in my Quarkus application, as described per the Quarkus guides:
#Singleton
public class ObjectMapperConfig implements ObjectMapperCustomizer {
#Override
public void customize(ObjectMapper objectMapper) {
objectMapper.enable(SerializationFeature.WRAP_ROOT_VALUE);
objectMapper.enable(DeserializationFeature.UNWRAP_ROOT_VALUE);
objectMapper.registerModule(new JavaTimeModule());
}
}
I've done this to wrap/unwrap my objects with the #JsonRootName annotation:
#RegisterForReflection
#JsonRootName("article")
public class CreateArticleRequest {
private CreateArticleRequest(String title, String description, String body, List<String> tagList) {
this.title = title;
this.description = description;
this.body = body;
this.tagList = tagList;
}
private String title;
private String description;
private String body;
private List<String> tagList;
...
}
This works just fine when curl against my actual API, but whenever I use RestAssured in one of my tests, RestAssured does not seem to respect my ObjectMapper config, and does not wrap the CreateArticleRequest as it should be doing as indicated by the #JsonRootName annotation.
#QuarkusTest
public class ArticleResourceTest {
#Test
public void testCreateArticle() {
given()
.when()
.body(CREATE_ARTICLE_REQUEST)
.contentType(ContentType.JSON)
.log().all()
.post("/api/articles")
.then()
.statusCode(201)
.body("", equalTo(""))
.body("article.title", equalTo(ARTICLE_TITLE))
.body("article.favorited", equalTo(ARTICLE_FAVORITE))
.body("article.body", equalTo(ARTICLE_BODY))
.body("article.favoritesCount", equalTo(ARTICLE_FAVORITE_COUNT))
.body("article.taglist", equalTo(ARTICLE_TAG_LIST));
}
}
This serializes my request body as:
{
"title": "How to train your dragon",
"description": "Ever wonder how?",
"body": "Very carefully.",
"tagList": [
"dragons",
"training"
]
}
... instead of ...
{
"article": {
"title": "How to train your dragon",
"description": "Ever wonder how?",
"body": "Very carefully.",
"tagList": [
"dragons",
"training"
]
}
}
I can actually fix this, by manually configuring the RestAssured ObjectMapper, like so:
#QuarkusTest
public class ArticleResourceTest {
#BeforeEach
void setUp() {
RestAssured.config = RestAssuredConfig.config().objectMapperConfig(new ObjectMapperConfig().jackson2ObjectMapperFactory(
(cls, charset) -> {
ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
mapper.enable(SerializationFeature.WRAP_ROOT_VALUE);
mapper.enable(DeserializationFeature.UNWRAP_ROOT_VALUE);
mapper.registerModule(new JavaTimeModule());
return mapper;
}
));
}
}
However, I obviously do not want to do this! I wanted RestAssured to pick up my ObjectMapper config so that I don't need to keep around two different ObjectMapper configurations.
Why is it not being picked up? What am I missing?
I solved this with
#QuarkusTest
public class RESTResourceTest {
#Inject
ObjectMapper mapper;
#BeforeEach
void setUp() {
RestAssured.config = RestAssured.config().objectMapperConfig(new ObjectMapperConfig().jackson2ObjectMapperFactory( (cls, charset) -> mapper ));
}
where somewhere else in my src/main/java I have my custom ObjectMapper customizer:
import javax.inject.Singleton;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.cloudevents.jackson.JsonFormat;
import io.quarkus.jackson.ObjectMapperCustomizer;
#Singleton
public class QuarkusObjectMapperCustomizer implements ObjectMapperCustomizer {
public void customize(ObjectMapper mapper) {
mapper.registerModule(JsonFormat.getCloudEventJacksonModule());
}
}
So actually Quarkus won't be doing this automatically (due to the fact that it would interfere with the native tests).
You can however use:
#Inject
ObjectMapper
inside the test to setup RestAssured.config
Just to update in case someone find this like me...
I just add the Singleton ObjectMapperConfig and this is working on
<quarkus.platform.version>2.3.0.Final</quarkus.platform.version>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
I want to consume a RESTful service that returns text/javascript content type.
Since there is no OOTB HttpMessageConverter which can do this in Spring Boot, I want to register a custom converter.
One way I found to do this is to customize the RestTemplate itself by modifying MappingJackson2HttpMessageConverter:
#Component
public class CustomRestTemplateProvider {
public RestTemplate getCustomRestTemplate(MediaType mediaType) {
MappingJackson2HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter();
jacksonConverter.setSupportedMediaTypes(Collections.singletonList(mediaType));
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(jacksonConverter);
return new RestTemplate(converters);
}
}
Then, in my service, just call getCustomRestTemplate(new MediaType("text", "javascript")
The above solution works fine but I also tried to create a new Converter which handles this one Media Type, according to the Spring Boot Documentation (27.1.2):
So I created a new Converter:
#Component
public class TextJavascriptMessageConverter extends AbstractJackson2HttpMessageConverter {
public TextJavascriptMessageConverter(ObjectMapper objectMapper) {
super(objectMapper);
setTextJavascriptAsSupportedMediaType();
}
private void setTextJavascriptAsSupportedMediaType() {
List<MediaType> supportedMediaTypes = new ArrayList<>();
supportedMediaTypes.add(new MediaType("text", "javascript"));
setSupportedMediaTypes(supportedMediaTypes);
}
}
Then tried to register it like in the documentation:
#Configuration
public class ApplicationConfiguration {
#Bean
public HttpMessageConverters customConverters() {
HttpMessageConverter<Object> converter = new TextJavascriptMessageConverter(new ObjectMapper());
return new HttpMessageConverters(Collections.singletonList(converter));
}
}
Still, I get a message that states Could not extract response: no suitable HttpMessageConverter found for response type [class example.MyResponsee] and content type [text/javascript]
I even tried to extend the MappingJackson2HttpMessageConverter, but that didn't work either. What am I missing? Also, is it a good practice to create a new converter for this, or modyfing an existing one (like I shown in the first example) is acceptable?
The documentation is a bit hazy but you should be able to just register your HttpMessageConverter bean and it will get added appropriately.
Source:
Any HttpMessageConverter bean that is present in the context will be added to the list of converters
Since you've already registered TextJavascriptMessageConverter as a bean (via #Component) you should be able to just autowire HttpMessageConverters for access to all the converters.
Though preferably you could autowire a RestTemplateBuilder which handles setting up restTemplate for you (including converters).
Example:
#SpringBootApplication
public class DemoApplication {
#Component
public static class TextJavascriptConverter extends AbstractJackson2HttpMessageConverter {
public TextJavascriptConverter() {
//can use overloaded constructor to set supported MediaType
super(new ObjectMapper(), new MediaType("text", "javascript"));
}
}
#Bean
public ApplicationRunner demoRunner(RestTemplateBuilder builder, TextJavascriptConverter javascriptConverter) {
return args -> {
//can autowire RestTemplateBuilder for sensible defaults (including converters)
RestTemplate restTemplate = builder.build();
//confirm your converter is there
if (restTemplate.getMessageConverters().contains(javascriptConverter)) {
System.out.println("My custom HttpMessageConverter was registered!");
}
};
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
Also, is it a good practice to create a new converter for this, or modyfing an existing one (like I shown in the first example) is acceptable?
You're probably better off creating your own converter otherwise you risk dropping support for the original media type(s).
I think you should use other constructor to explicitly designate the media type.
protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper) {
init(objectMapper);
}
protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType supportedMediaType) {
super(supportedMediaType);
init(objectMapper);
}
protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType... supportedMediaTypes) {
super(supportedMediaTypes);
init(objectMapper);
}
You should be able to define a new MappingJackson2HttpMessageConverter bean, that will be picked up by Boot and loaded over the default.
#Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(){
MappingJackson2HttpMessageConverter jacksonConverter = new
MappingJackson2HttpMessageConverter();
jacksonConverter.setSupportedMediaTypes
(Collections.singletonList(mediaType));
return jacksonConverter;
}
I have a project with spring mvc and i wanna invoke method "setIgnorableProperties" from MapDeserializer globally, but I dont know how get this class from ObjectMapper, can you help me? Thx for advice.
I see it, like that:
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
mapDeserializer.getContentType();
converters.forEach(httpMessageConverter -> {
if (httpMessageConverter instanceof MappingJackson2HttpMessageConverter) {
MappingJackson2HttpMessageConverter converter = (MappingJackson2HttpMessageConverter) httpMessageConverter;
ObjectMapper mapper = converter.getObjectMapper();
MapDeserializer mapDes = mapper.(What I have to invoke?) ;
mapDes.setIgnorableProperties({"#id", "#ref"});
}
});
}
That property is not meant to be configured directly; you will need to use #JsonIgnoreProperties annotation for Map-valued properties.
You can create convenience annotation, if you want, by:
#Retention(RetentionPolicy.RUNTIME) // IMPORTANT
#JacksonAnnotationsInside
#JsonIgnoreProperties({ "#id", "#ref" })
public #interface MapIgnorals
and then use like:
public class Stuff {
#MapIgnorals public Map<String,Object> values;
}