How to pass argument to custom JSON deserializer in spring boot app - java

I want to use my own deserializer in spring boot rest controller. To do its job, it needs some custom configuration - which is provided to it as constructor argument. How can I pass such argument in rest controller?
Here is example.
DTO(with some lombok annotations):
#Getter
#Setter
#RequiredArgsConstructor
#AllArgsConstructor
#JsonDeserialize(using = Deserializer.class)
public class DTO {
private int a;
private int b;
}
Deserializer:
public class Deserializer extends JsonDeserializer<DTO> {
//custom config
int val;
public Deserializer(int value) {
val = value;
}
#Override
public DTO deserialize(JsonParser p, DeserializationContext ctxt) throws IOException{
JsonNode node = p.readValueAsTree();
int a = node.has("a") ? node.get("a").asInt() : -1;
int b = node.has("b") ? node.get("b").asInt() : -1;
//custom config usage
return new DTO(a + val, b + val);
}
}
Controller:
#RestController
#RequestMapping
public class Controller {
//how to pass `val` into deserializer of DTO object?
#PostMapping("/foo")
DTO foo(#RequestBody DTO dto) {
return dto;
}
}
Any help would be appreciated.

you can create a custom ObjectMapper and add your custom serializer to it and at the same time load in a custom value from application.properties.
I think this should work, wrote it from the top of my head.
#Configuration
public class JacksonConfiguration {
#Value("${customValue}")
private int myCustomValue;
#Bean
public ObjectMapper objectMapper() {
final ObjectMapper mapper = new ObjectMapper();
final SimpleModule module = new SimpleModule();
module.addSerializer(new Deserializer(myCustomValue));
mapper.registerModule(module);
return mapper;
}
}

Related

How to save java object in postgres jsonb column

Here is my code:
I need to save java object value as jsonb in database (r2dbc).
#Getter
#Setter
#ToString
#NoArgsConstructor
#AllArgsConstructor
#Table("scoring")
public class ScoringModel extends BaseModel {
#Column("client_id")
#SerializedName(value = "clientId", alternate = {"client_id"})
private String clientId;
//othres
#Column("languages")
#SerializedName(value = "languages", alternate = {"languages"})
private String languages;
#SerializedName(value = "response", alternate = {"response"})
//Need to save as JsonB
private Object response;
}
Please help me resolve the issue
You need to implement ReadingConverter and WritingConverter and then register them in R2dbcCustomConversions in configuration.
#Bean
public R2dbcCustomConversions myConverters(ConnectionFactory connectionFactory){
var dialect = DialectResolver.getDialect(connectionFactory);
var converters = List.of(…);
return R2dbcCustomConversions.of(dialect, converters);
}
Converters itself should produce JSONs.
If you using Postgres, there are two approaches you can use to map to Postgres JSON/JSONB fields.
use Postgres R2dbc Json type directly in Java entity class.
use any Java type and convert it between Json via registering custom converters.
The first one is simple and stupid.
Declare a json db type field, eg.
metadata JSON default '{}'
Declare the Json type field in your entity class.
class Post{
#Column("metadata")
private Json metadata;
}
For the second the solution, similarly
Declare json/jsonb db type in the schema.sql.
Declare the field type as your custom type.eg.
class Post{
#Column("statistics")
private Statistics statistics;
record Statistics(
Integer viewed,
Integer bookmarked
) {}
}
Declare a R2dbcCustomConversions bean.
#Configuration
class DataR2dbcConfig {
#Bean
R2dbcCustomConversions r2dbcCustomConversions(ConnectionFactory factory, ObjectMapper objectMapper) {
R2dbcDialect dialect = DialectResolver.getDialect(factory);
return R2dbcCustomConversions
.of(
dialect,
List.of(
new JsonToStatisticsConverter(objectMapper),
new StatisticsToJsonConverter(objectMapper)
)
);
}
}
#ReadingConverter
#RequiredArgsConstructor
class JsonToStatisticsConverter implements Converter<Json, Post.Statistics> {
private final ObjectMapper objectMapper;
#SneakyThrows
#Override
public Post.Statistics convert(Json source) {
return objectMapper.readValue(source.asString(), Post.Statistics.class);
}
}
#WritingConverter
#RequiredArgsConstructor
class StatisticsToJsonConverter implements Converter<Post.Statistics, Json> {
private final ObjectMapper objectMapper;
#SneakyThrows
#Override
public Json convert(Post.Statistics source) {
return Json.of(objectMapper.writeValueAsString(source));
}
}
The example codes is here.
Finally verify it with a #DataR2dbcTest test.
#Test
public void testInsertAndQuery() {
var data = Post.builder()
.title("test title")
.content("content of test")
.metadata(Json.of("{\"tags\":[\"spring\",\"r2dbc\"]}"))
.statistics(new Post.Statistics(1000, 200))
.build();
this.template.insert(data)
.thenMany(
this.posts.findByTitleContains("test%")
)
.log()
.as(StepVerifier::create)
.consumeNextWith(p -> {
log.info("saved post: {}", p);
assertThat(p.getTitle()).isEqualTo("test title");
}
)
.verifyComplete();
}

Lombok with JsonDeserializer

I have a JSON string that I want to deserialize into a class. The JSON looks like so:
{ "data": { "name": "Box 1", "size": "10x20" } }
I can deserialize this into the following class:
#Builder
#Value
#JsonDeserialize(builder = Box1.Box1Builder.class)
public class Box1 {
#JsonProperty("data")
Box1Data data;
public static Box1 of(String json) throws IOException {
return new ObjectMapper().readValue(json, Box1.class);
}
#Builder
#Value
#JsonDeserialize(builder = Box1Data.Box1DataBuilder.class)
static class Box1Data {
#JsonProperty("name")
String name;
#JsonProperty("size")
String size;
}
}
The above class looks clumsy since it has a useless hierarchy of data. I can get rid of it like so:
#Builder
#Value
#JsonDeserialize(using = Box2Deserializer.class)
public class Box2 {
#JsonProperty("name")
String name;
#JsonProperty("size")
String size;
public static Box2 of(String json) throws IOException {
return new ObjectMapper().readValue(json, Box2.class);
}
static class Box2Deserializer extends JsonDeserializer<Box2> {
#Override
public Box2 deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
var node = jsonParser.getCodec().readTree(jsonParser);
var dataNode = node.get("data");
return Box2.builder()
.name(dataNode.get("name").toString())
.size(dataNode.get("size").toString())
.build();
}
}
}
But here, I hit a dead-end. I want the size field to be parsed into a Dimension instance. I can write a custom deserializer for size that parses a String and returns a proper Dimension, but I cannot use it via field annotations (#JsonDeserialize(using = SizeDeserializer.class) since the presence of JsonDeserialize class annotation forces it to be ignored in the case for Box1, and in the case for Box2, it's ignored cuz I'm building the box manually.
Is there an elegant solution to all this mess? What I want is to read the given JSON into a class like this:
#Builder
#Value
public class Box3 {
#JsonProperty("name")
String name;
#JsonProperty("size")
Dimension size;
public static Box3 of(String json) {
...
}
}
Thanks!
Asim
I will add to #Iprakashv solution, besides only the needs for the JsonRootName type annotation and mapper serialization / deserialization for root node wrapping, you only need a custom type converter from a raw type to your custom type:
#Builder
#Value
#JsonRootName("data")
public class Box {
#JsonProperty("name")
String name;
#JsonDeserialize(converter = StringToDimensionConverter.class)
#JsonProperty("size")
Dimension size;
public static Box of(String json) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(DeserializationFeature.UNWRAP_ROOT_VALUE);
mapper.enable(SerializationFeature.WRAP_ROOT_VALUE);
return mapper.readValue(json, Box.class);
}
private static class StringToDimensionConverter extends StdConverter<String, Dimension> {
#Override
public DataWrapper.Box1Data.Dimension convert(String s) {
return new DataWrapper.Box1Data.Dimension(s);
}
}
}
You actually do not need a custom deserializer and the #JsonDeserialize annotation. The ObjectMapper provides a configuration to enable wrapping/unwrapping a root value which can be provided using the #JsonRootName annotation over the Wrapper object class.
#Builder
#Value
#JsonRootName("data")
public class Box {
#JsonProperty("name")
String name;
#JsonProperty("size")
String size;
public static Box of(String json) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(DeserializationFeature.UNWRAP_ROOT_VALUE);
return mapper.readValue(json, Box.class);
}
}
PS: Totally missed the Dimension part in the question, for that, you can use a custom deserializer as mentioned in other answer.

How to customize json wrapped key for class during deserialization if I am not able to mark that class with annotation?

I have model class like this:
import org.codehaus.jackson.map.*;
public class MyPojo {
int id;
public int getId()
{ return this.id; }
public void setId(int id)
{ this.id = id; }
}
And json json has format like this:
{
"root":{
"MyPojo":{
"id":4
}
}
}
I read about solution via annotations #JsonTypeInfo or #JsonRoot but I am not able to apply this because I take this class from library.
Also I read about solution like this:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(DeserializationFeature.UNWRAP_ROOT_VALUE);
But I can't use it because:
1. It is setting for all classes but I need to change behaviour only for 1 class right now
2. It will check that root key corresponds the class name(mypojo in my case) but I it is not true for me.
Any ideas?
The way to go here is adding a custom deserializer. There you can manipulate json programatically to create your MyPojo instance
public class MyPojoDeserializer extends StdDeserializer<MyPojo> {
#Override
public Item deserialize(JsonParser jp, DeserializationContext ctxt) {
JsonNode node = jp.getCodec().readTree(jp);
return new MyPojo(node.get("root").get("MyPojo").get("id").numberValue());
}
}
then register deserializer
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(MyPojo.class, new MyPojoDeserializer());
mapper.registerModule(module);

Using #JsonIdentityInfo without annotations

I use Jackson 2.2.3 to serialize POJOs to JSON. Then I had the problem, that I couldn't serialize recursive structures...I solved this problem by using #JsonIdentityInfo => works great.
But, I don't want this annotation on the top of my POJO.
So my question is: Is there any other possibility to set the default behavior of my ObjectMapper to use the feature for every POJO?
So I want to transform this annotation code
#JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="#id")
to something like
ObjectMapper om = new ObjectMapper();
om.setDefaultIdentityInfo(ObjectIdGenerators.IntSequenceGenerator.class, "#id");
Any ideas?
You can achieve that using the Jackson mix-in annotations or the Jackson annotation introspector.
Here is an example showing both methods:
public class JacksonJsonIdentityInfo {
#JsonIdentityInfo(
generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "#id")
static class Bean {
public final String field;
public Bean(final String field) {this.field = field;}
}
static class Bean2 {
public final String field2;
public Bean2(final String field2) {this.field2 = field2;}
}
#JsonIdentityInfo(
generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "#id2")
static interface Bean2MixIn {
}
static class Bean3 {
public final String field3;
public Bean3(final String field3) {this.field3 = field3;}
}
static class MyJacksonAnnotationIntrospector extends JacksonAnnotationIntrospector {
#Override
public ObjectIdInfo findObjectIdInfo(final Annotated ann) {
if (ann.getRawType() == Bean3.class) {
return new ObjectIdInfo(
PropertyName.construct("#id3", null),
null,
ObjectIdGenerators.IntSequenceGenerator.class,
null);
}
return super.findObjectIdInfo(ann);
}
}
public static void main(String[] args) throws JsonProcessingException {
final Bean bean = new Bean("value");
final Bean2 bean2 = new Bean2("value2");
final Bean3 bean3 = new Bean3("value3");
final ObjectMapper mapper = new ObjectMapper();
mapper.addMixInAnnotations(Bean2.class, Bean2MixIn.class);
mapper.setAnnotationIntrospector(new MyJacksonAnnotationIntrospector());
System.out.println(mapper.writeValueAsString(bean));
System.out.println(mapper.writeValueAsString(bean2));
System.out.println(mapper.writeValueAsString(bean3));
}
}
Output:
{"#id":1,"field":"value"}
{"#id2":1,"field2":"value2"}
{"#id3":1,"field3":"value3"}
After several months and a lot of research, I've implemented my own solution to keep my domain clear of jackson dependencies.
public class Parent {
private Child child;
public Child getChild(){return child;}
public void setChild(Child child){this.child=child;}
}
public class Child {
private Parent parent;
public Child getParent(){return parent;}
public void setParent(Parent parent){this.parent=parent;}
}
First, you have to declare each of your entities of the bidirectional relationship:
public interface BidirectionalDefinition {
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id", scope=Parent.class)
public interface ParentDef{};
#JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id", scope=Child.class)
public interface ChildDef{};
}
After that, the object mapper can be automatically configured:
ObjectMapper om = new ObjectMapper();
Class<?>[] definitions = BidirectionalDefinition.class.getDeclaredClasses();
for (Class<?> definition : definitions) {
om.addMixInAnnotations(definition.getAnnotation(JsonIdentityInfo.class).scope(), definition);
}

Spring #RestController custom JSON deserializer

I want to use custom JSON deserializer for some classes(Role here) but I can't get it working. The custom deserializer just isn't called.
I use Spring Boot 1.2.
Deserializer:
public class ModelDeserializer extends JsonDeserializer<Role> {
#Override
public Role deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
return null; // this is what should be called but it isn't
}
}
Controller:
#RestController
public class RoleController {
#RequestMapping(value = "/role", method = RequestMethod.POST)
public Object createRole(Role role) {
// ... this is called
}
}
#JsonDeserialize on Role
#JsonDeserialize(using = ModelDeserializer.class)
public class Role extends Model {
}
Jackson2ObjectMapperBuilder bean in Java Config
#Bean
public Jackson2ObjectMapperBuilder jacksonBuilder() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.deserializerByType(Role.class, new ModelDeserializer());
return builder;
}
What am I doing wrong?
EDIT It is probably caused by #RestController because it works with #Controller...
First of all you don't need to override Jackson2ObjectMapperBuilder to add custom deserializer. This approach should be used when you can't add #JsonDeserialize annotation. You should use #JsonDeserialize or override Jackson2ObjectMapperBuilder.
What is missed is the #RequestBody annotation:
#RestController
public class JacksonCustomDesRestEndpoint {
#RequestMapping(value = "/role", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public Object createRole(#RequestBody Role role) {
return role;
}
}
#JsonDeserialize(using = RoleDeserializer.class)
public class Role {
// ......
}
public class RoleDeserializer extends JsonDeserializer<Role> {
#Override
public Role deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
// .................
return something;
}
}
There is also another pretty interesting solution which can be helpful in case when you want to modify your JSON body before calling default deserializer. And let's imagine that you need to use some additional bean for that (use #Autowire mechanism)
Let's image situation, that you have the following controller:
#RequestMapping(value = "/order/product", method = POST)
public <T extends OrderProductInterface> RestGenericResponse orderProduct(#RequestBody #Valid T data) {
orderService.orderProduct(data);
return generateResponse();
}
Where OrderProductInterface is:
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonSerialize(include = NON_EMPTY)
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, visible = true, property = "providerType")
#JsonSubTypes({
#JsonSubTypes.Type(value = OrderProductForARequestData.class, name = "A")
})
public interface OrderProductInterface{}
The code above will provide dynamic deserialization base on filed providerType and validation according to concrete implementation. For better grasp, consider that OrderProductForARequestData can be something like that:
public class OrderProductForARequestData implements OrderProductInterface {
#NotBlank(message = "is mandatory field.")
#Getter #Setter
private String providerId;
#NotBlank(message = "is mandatory field.")
#Getter #Setter
private String providerType;
#NotBlank(message = "is mandatory field.")
#Getter #Setter
private String productToOrder;
}
And let's image now that we want to init somehow providerType (enrich input) before default deserialization will be executed. so the object will be deserialized properly according to the rule in OrderProductInterface.
To do that you can just modify your #Configuration class in the following way:
//here can be any annotation which will enable MVC/Boot
#Configuration
public class YourConfiguration{
#Autowired
private ObjectMapper mapper;
#Autowired
private ProviderService providerService;
#Override
public void setup() {
super.setup();
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
if (beanDesc.getBeanClass() == OrderProductInterface.class) {
return new OrderProductInterfaceDeserializer(providerService, beanDesc);
}
return deserializer;
}
});
mapper.registerModule(module);
}
public static class OrderProductInterfaceDeserializer extends AbstractDeserializer {
private static final long serialVersionUID = 7923585097068641765L;
private final ProviderService providerService;
OrderProductInterfaceDeserializer(roviderService providerService, BeanDescription beanDescription) {
super(beanDescription);
this.providerService = providerService;
}
#Override
public Object deserializeWithType(JsonParser p, DeserializationContext context, TypeDeserializer typeDeserializer) throws IOException {
ObjectCodec oc = p.getCodec();
JsonNode node = oc.readTree(p);
//Let's image that we have some identifier for provider type and we want to detect it
JsonNode tmp = node.get("providerId");
Assert.notNull(tmp, "'providerId' is mandatory field");
String providerId = tmp.textValue();
Assert.hasText(providerId, "'providerId' can't be empty");
// Modify node
((ObjectNode) node).put("providerType",providerService.getProvider(providerId));
JsonFactory jsonFactory = new JsonFactory();
JsonParser newParser = jsonFactory.createParser(node.toString());
newParser.nextToken();
return super.deserializeWithType(newParser, context, typeDeserializer);
}
}
}

Categories