SpringBoot: Consume & Produce XML with a Custom Serializer + Deserializer - java

I have a SpringBoot Service with:
Model
public class Payload {
private final String id;
public Payload(String id){
this.id = id;
}
public String getId() {
return this.id;
}
}
Controller
#RestController
#RequestMapping("/payload")
public class PayloadController {
#RequestMapping(method = RequestMethod.POST)
public Payload post(#RequestBody final Payload payload) {
return payload;
}
}
I need this Controller to be able to handle JSON & XML requests and respond with the same format.
This works fine providing I set the Content-Type and Accept headers to the correct media types.
However, my XML payloads need to be in a subtly different structure to my JSON:
XML:
<Payload>
<id value="some-value"/>
</Payload>
JSON:
{
id: "some-value"
}
How do I ensure my id is wrapped in an xml node and has the "value" as an attribute?
I have tried using a #JsonSerialize and #JsonDeserialize annotation on my Payload class but as soon as I do this I get the following error when POSTing XML
{
"timestamp": "2019-10-01T12:06:35.593+0000",
"status": 415,
"error": "Unsupported Media Type",
"message": "Content type 'application/xml;charset=UTF-8' not supported",
"path": "/payload"
}

You need to register 2 converters:
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter for JSON.
org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter for XML.
Because, Payload class fits JSON payload you need to add only JsonCreator and JsonProperty annotations to make it work:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Payload {
private final String id;
#JsonCreator
public Payload(#JsonProperty(value = "id") String id) {
this.id = id;
}
public String getId() {
return this.id;
}
}
XML payload does not fit by default, so we need to implement custom serialiser:
import com.example.demo.model.Payload;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;
import java.io.IOException;
public class PayloadXmlSerializer extends JsonSerializer<Payload> {
#Override
public void serialize(Payload value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
ToXmlGenerator toXmlGenerator = (ToXmlGenerator) gen;
toXmlGenerator.writeStartObject();
toXmlGenerator.writeObjectFieldStart("id");
toXmlGenerator.setNextIsAttribute(true);
toXmlGenerator.writeFieldName("value");
toXmlGenerator.writeString(value.getId());
toXmlGenerator.setNextIsAttribute(false);
toXmlGenerator.writeEndObject();
toXmlGenerator.writeEndObject();
}
}
and deserialiser:
import com.example.demo.model.Payload;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonPointer;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.node.TextNode;
import java.io.IOException;
public class PayloadXmlDeserializer extends JsonDeserializer<Payload> {
#Override
public Payload deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
TreeNode root = p.readValueAsTree();
TreeNode value = root.at(JsonPointer.compile("/id/value"));
if (value.isMissingNode()) {
return new Payload(null);
}
TextNode textNode = (TextNode)value;
return new Payload(textNode.textValue());
}
}
Finally, we need to register above HTTP converters and custom serialiser/deserialiser:
import com.example.demo.model.Payload;
import com.example.jackson.PayloadXmlDeserializer;
import com.example.jackson.PayloadXmlSerializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
#EnableWebMvc
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//JSON
converters.add(new MappingJackson2HttpMessageConverter());
// XML
converters.add(new MappingJackson2XmlHttpMessageConverter(Jackson2ObjectMapperBuilder
.xml()
.modules(payloadModule())
.build()));
}
public SimpleModule payloadModule() {
SimpleModule module = new SimpleModule();
module.addDeserializer(Payload.class, new PayloadXmlDeserializer());
module.addSerializer(Payload.class, new PayloadXmlSerializer());
return module;
}
}
See also:
Using Jackson to add XML attributes to manually-built node-tree
415 Unsupported MediaType for POST request in spring application
Spring MVC

Related

org.springframework.web.client.RestClientException: Error while extracting response for type and content type [application/json;charset=utf-8]

I can't figure out what the problem is. I am using postgre DB. When I run a test for the GET method, an error occurs, for the second day I can not solve it.
Here is my Entity class
import lombok.*;
import lombok.experimental.FieldDefaults;
import javax.persistence.*;
#Entity
#Table(name = "cities_catalog")
#FieldDefaults(level = AccessLevel.PRIVATE)
#Data
#NoArgsConstructor
public class PostgreCity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Integer id;
#Column(name = "name")
String name;
public PostgreCity(String name) {
this.name = name;
}
}
here is my Repository class
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
#Repository
public interface CityRepository extends JpaRepository<PostgreCity, Integer> {
}
here is my Controller class
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.experimental.FieldDefaults;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
#RestController
#AllArgsConstructor
#FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class CityPostController {
#Autowired
CityRepository cityRepository;
#GetMapping(value = "/get")
public List<PostgreCity> get(){
List<PostgreCity> list = this.cityRepository.findAll();
return list;
}
}
here is my Junit test class
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import java.util.HashMap;
import static org.junit.jupiter.api.Assertions.*;
class CityPostControllerTest extends RequestService {
#Autowired
CityRepository cityRepositoryp;
Integer id;
#BeforeEach
void setUp() {
}
#AfterEach
void tearDown() {
}
#Test
void get() {
ResponseEntity<PostgreCity> responseEntity = this.get(PostgreCity.class);
assertNotNull(responseEntity);
assertEquals(HttpStatus.OK.value(), responseEntity.getStatusCodeValue());
}
#Override
public String getPath() {
return "/get";
}
}
here is my RequestService class
import io.egrow.eugene.insurance.InsuranceApplicationTests;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
public abstract class RequestService extends InsuranceApplicationTests {
#Autowired
TestRestTemplate testRestTemplate;
public <T> ResponseEntity<T> patchNoAuth(String payload, Class<T> tClass) {
HttpHeaders headers = getHeaderWithoutAuthentication();
HttpEntity<String> entity = new HttpEntity<>(payload, headers);
return testRestTemplate.exchange(this.getPath(), HttpMethod.PATCH, entity, tClass);
}
public <T> ResponseEntity<T> get(Class<T> tClass) {
return testRestTemplate.getForEntity(this.getPath(), tClass);
}
private HttpHeaders getHeaderWithoutAuthentication() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
public abstract String getPath();
}
here is error message when I run test.
org.springframework.web.client.RestClientException: Error while extracting response for type [class io.egrow.eugene.insurance.boundary.databases.postgre.models.cities.PostgreCity] and content type [application/json;charset=utf-8]; nested exception is org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `io.egrow.eugene.insurance.boundary.databases.postgre.models.cities.PostgreCity` from Array value (token `JsonToken.START_ARRAY`); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type `io.egrow.eugene.insurance.boundary.databases.postgre.models.cities.PostgreCity` from Array value (token `JsonToken.START_ARRAY`)
at [Source: (PushbackInputStream); line: 1, column: 1]
The problem is here:
ResponseEntity<PostgreCity> responseEntity = this.get(PostgreCity.class);
You are expecting a single entity but in the RestController you have a List:
#GetMapping(value = "/get")
public List<PostgreCity> get(){
List<PostgreCity> list = this.cityRepository.findAll();
return list;
}
For getting the list you can use the ParameterizedTypeReference, like so:
ResponseEntity<List<PostgreCity>> responseEntity =
restTemplate.exchange(
"/get",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<PostgreCity>>() {}
);
List<PostgreCity> postgreCities = responseEntity.getBody();
For more details and reference can have a look at this tutorial:
https://www.baeldung.com/spring-resttemplate-json-list

How to provide completely custom JSON for example in Swagger?

I have Java endpoint which receives json-deserializable object. Unfortunately, Swagger is unable to auto-generate good example for it. Is it possible to provide completely custom JSON for an example?
Example is below, regard class Body. It has two fields.
One field is a Set. I want to provide some example list of values for it. I can't use example parameter for this.
Another field is a Parent. It can contain one of two of subclessed, Child1 and Child2. Springfox generates me
{
"parent": {
"#child#": "string"
},
"tags": "[\"tag1\", \"tag2\"]"
}
and I can't send this value (it's incorrect serialization). While I want to have
{
"parent": {
"#child#": "1",
"field1": "value of field 1"
},
"tags": ["tag1", "tag2"]
}
The code:
package com.example.demo;
import java.io.IOException;
import java.util.Set;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.DatabindContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
#RestController
#SpringBootApplication
#Configuration
#EnableOpenApi
public class DemoApplication {
#PostMapping(value = "/create", consumes = MediaType.APPLICATION_JSON_VALUE)
public Body create(#RequestBody Body body) {
return body;
}
#Bean
public Docket docket() {
return new Docket(DocumentationType.OAS_30)
.select()
.apis(RequestHandlerSelectors.basePackage(DemoApplication.class.getPackageName()))
.paths(PathSelectors.any())
.build()
//.apiInfo(apiInfo())
//.securitySchemes(Collections.singletonList(apiKey()))
//.protocols(getProtocols(systemSettings))
;
}
public static class Body {
#ApiModelProperty(example = "[\"tag1\", \"tag2\"]")
public Set<String> tags;
public Parent parent;
}
#JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, property = "#child#", include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true)
#JsonTypeIdResolver(MyTypeIdResolver.class)
#ApiModel(discriminator = "#child#")
public static class Parent {
final String childTypeNumber;
#JsonProperty("#child#")
public String childTypeNumber() {
return childTypeNumber;
}
public Parent(String childTypeNumber) {
this.childTypeNumber = childTypeNumber;
}
}
public static class MyTypeIdResolver extends TypeIdResolverBase {
private JavaType superType;
#Override
public void init(JavaType baseType) {
superType = baseType;
}
#Override
public String idFromValue(Object value) {
return null;
}
#Override
public String idFromValueAndType(Object value, Class<?> suggestedType) {
return null;
}
#Override
public JsonTypeInfo.Id getMechanism() {
return null;
}
#Override
public JavaType typeFromId(DatabindContext context, String id) throws IOException {
char c = id.charAt(0);
Class<?> subType = null;
switch (c) {
case '1':
subType = Child1.class;
break;
case '2':
subType = Child2.class;
break;
default:
throw new RuntimeException("Invalid Child type");
}
return context.constructSpecializedType(superType, subType);
}
}
public static class Child1 extends Parent {
public String field1;
public Child1() {
super("1");
}
}
public static class Child2 extends Parent {
public String field2;
public Child2() {
super("2");
}
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
From what I understand, you want swagger to display the resource returned by the endpoint.
If so, this is the solution:
#Operation(summary = "create new resource",
description = "create resourcey completely", responses = {
#ApiResponse(responseCode = "200",
description = "createresource",
content = {#Content(mediaType = "application/json",
schema = #Schema(implementation = Body.class))})
#PostMapping(value = "/create", consumes = MediaType.APPLICATION_JSON_VALUE)
public Body create(#RequestBody Body body) {
return body;
}
So that the controller does not have so many things left, what is done is to create the controller interface with all the annotations on the method signature, then your controller will implement the interface that already has all the documentation annotations.

Jackson ignoring specific property but able to check was it availble

Is there a way to skip some properties on deserialization but at the same time knowing are they presented or not?
{
"id": 123,
"name": "My Name",
"picture": {
// a lot of properties that's not important for me
}
}
#JsonIgnoreProperties(ignoreUnknown=true)
#JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
private int id;
}
So, I ignoreUnknown is what I want as a default behavior because I don't want name field and all other fields that can exist. The value of picture fields also is not important. I just want to know was picture property available or not. How I can do that?
You can add a boolean property and custom deserializer which just reads given value and returns true. Jackson invokes custom deserializer only if property exists in payload.
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import java.io.File;
import java.io.IOException;
public class JsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./src/main/resources/test.json");
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.readValue(jsonFile, User.class));
}
}
class PropertyExistsJsonDeserializer extends JsonDeserializer<Boolean> {
#Override
public Boolean deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
p.readValueAsTree(); //consume value
return Boolean.TRUE;
}
}
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonInclude(JsonInclude.Include.NON_NULL)
class User {
private int id;
#JsonDeserialize(using = PropertyExistsJsonDeserializer.class)
#JsonProperty("picture")
private boolean pictureAvailable;
//getters, setters, toString
}
Above code prints:
User{id=123, pictureAvailable=true}

How to make jsonData case insensitive and in Spring MVC

New to spring ,
i am trying to access json object in #RequestBody MYPOJO pojo which works fine , but my json data needed to be same as variable name in pojo and case sensitive. best i did find from web is here , but not synchronize with my project , i am using spring mvc. So how can i make case insensitive my json with pojo?
the way i receive json
#RequestMapping(value = "create", method = RequestMethod.POST)
public void createPost(HttpServletRequest req, HttpServletResponse resp, #Valid #RequestBody Post post,
Errors errors) throws CustomException, IOException {
json data
function jsonForPost(isEdit, id) {
var post = {};
if (isEdit) {
post.id = id;
}
post.name = $("#name").val();
return JSON.stringify(post);
}
With Spring Boot
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import com.fasterxml.jackson.databind.MapperFeature;
#Configuration
class Configs {
#Bean
public Jackson2ObjectMapperBuilderCustomizer initJackson() {
Jackson2ObjectMapperBuilderCustomizer c = new Jackson2ObjectMapperBuilderCustomizer() {
#Override
public void customize(Jackson2ObjectMapperBuilder builder) {
builder.featuresToEnable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
}
};
return c;
}
}
Without Spring Boot
import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.fasterxml.jackson.databind.MapperFeature;
#Configuration
#EnableWebMvc
public class AppConfig extends WebMvcConfigurerAdapter {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.indentOutput(true);
builder.featuresToEnable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
}
}
I have a POJO with a variable name in it:
public class Pox {
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
and a Controller:
#RequestMapping(value = "/create", method = RequestMethod.POST)
public void createPost(HttpServletRequest req, HttpServletResponse resp, #Valid #RequestBody Pox post,
Errors errors) {
System.out.println(post.getName());
}
I have tested with Postman with:
name, Name, NAme, nAme.
All of them worked.
With springboot using the application.yml file =>
spring:
jackson:
mapper:
accept-case-insensitive-properties: true

JAX-RS #Produces: how to customize JSON?

I am new to JAX-RS and I want to serve my list of items as JSON. My entity model is something like this:
public class Entity {
private String name;
private Date date;
private Float number;
}
This is how I am invoking the service:
#Path("/entities")
public class EntitiesController {
#GET
#Produces({"application/json"})
public List<Entity> getEntities() {
return EntityDAO.entitiesList();
}
}
However, the date is not formatted but is displayed as a long.
This answer shows how to format a date using a JsonSerializer. If I extend JsonSerializer, then where do I put that subclass in my project?
I figured a solution myself:
Under a new serializers package I created the CustomJsonDateSerializer class, which will be delegated the responsibility of formatting the date attribute thanks to the #JsonSerialize(...) annotation.
So I modified my Entity class adding that annotation ontop of the field:
#JsonSerialize(using = CustomJsonDateSerializer.class)
private Date date;
And this is the content of CustomJsonDateSerializer:
package serializers;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
public class CustomJsonDateSerializer extends JsonSerializer<Date> {
#Override
public void serialize(Date value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonGenerationException {
SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyy");
String format = formatter.format(value);
jgen.writeString(format);
}
}

Categories