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();
}
Related
I am doing testing with Wiremock and I have following example for POJO:
#Data
public class DataResponse {
private Supply supply;
private static class Supply {
private List<TimeSeries> timeSeries;
}
#JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public static class TimeSeries {
private LocalDate date;
}
}
And I am stubbing some external API which should response with this POJO as json:
DataResponse dataResponse = DataResponse.builder()
.supply(DataResponse.Supply.builder()
.timeSeries(List.of(
DataResponse.TimeSeries.builder()
.date("2021-11-16")
build()
))
.build()
.build()
String jsonDataResponse = mapper.writeValueAsString(dataResponse); //mapper is instance of ObjectMapper
stubFor(WireMock.get(urlEqualTo("/someUrl"))
.willReturn(aResponse()
.withBody(jsonDataResponse)));
The issue is when I do serialization with ObjectMapper I get "timeSeries" instead of "time_series". Is there any way to get the right JSON string format only with changes in the test file?
timeSeries is field of Supply, you should change place of #JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class), move to Supply class like below
#Data
public class DataResponse {
private Supply supply;
#JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
private static class Supply {
private List<TimeSeries> timeSeries;
}
public static class TimeSeries {
private LocalDate date;
}
}
I get a JSON string that I convert to an object. One of the property within the JSON sometimes would be null. If the property is null, I want the default value to be set to 0.
This is my class:
#Data
#NoArgsConstructor
#AllArgsConstructor
#Builder
public class Car {
#JsonProperty(value = "car_name")
private String carName;
#JsonProperty(value = "car_value")
private Long carValue;
#JsonProperty(value = "Specifications")
private Map<String, String> Specifications;
}
I use object mapper to convert the JSON string to the object
public List<Car> stringToCar(String json) throws JsonProcessingException {
ObjectMapper om = new ObjectMapper();
return om.readValue(json, new TypeReference<List<Car>>() {} );
}
carValue would sometimes have null value, if that happens I want it be set as 0. Is it possible to do in a efficient way rather than looping through the object and manually setting the value to 0
You have multiple ways to do this.
1) Setter that receives long instead
This is actually not straightforward but it works. If you define the setter as follows it will do what you need:
public void setCarValue(long carValue) {
this.carValue = carValue;
}
However, this feels like a hack to me, so I would not suggest you use it.
2) Custom deserializer
This one is more complex but also much easier to understand and explicit about your intent.
public class CustomLongDeserializer extends JsonDeserializer<Long> {
#Override
public Long deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
String text = jp.getText();
if (text == null || text.isEmpty()) {
return 0;
} else {
return Long.valueOf(text);
}
}
}
Then you could apply the serializer on the attribute as follows:
#Data
#NoArgsConstructor
#AllArgsConstructor
#Builder
public class Car {
#JsonProperty(value = "car_name")
private String carName;
#JsonProperty(value = "car_value")
#JsonDeserialize(using = CustomLongDeserializer.class)
private Long carValue;
#JsonProperty(value = "Specifications")
private Map<String, String> Specifications;
}
Or apply it as a global deserializer to be used to deserialize every single Long:
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Long.class, new CustomLongDeserializer());
mapper.registerModule(module);
It's not a generic solution, but in your specific use case you could use the long primitive instead of Long which will coerce null to 0 when deserialized.
If you don't want to create a separate class like #João Dias mentioned, you can define the constructor yourself instead of using #AllArgsConstructor, something like :
public Car(String carName,
Long carValue,
Map<String, String> Specifications){
this.carName = carName;
this.Specifications = Specifications;
this.carValue = (carValue == null ? 0 : carValue);
}
The same can be used for #Builder annotation like :
builder().carValue(value == null ? 0 : value).build();
Again, if you have more variables to handle you should consider previous response.
In our spring data projects we have "standard" approach to writing our DTOs, where we use lombok's #Value and #Builder for immutability and #JsonDeserialize(builder = SomeClass.SomeClassBuilder.class) for jackson deserialization.
Here is a minimal example:
#RestController
class Controller {
#PostMapping("/post")
void post(#RequestBody PostBody body) {
System.out.println(body);
}
}
#Value
#Builder
#JsonDeserialize(builder = PostBody.PostBodyBuilder.class)
class PostBody {
byte[] id;
ClientData clientData;
#JsonPOJOBuilder(withPrefix = "")
public static class PostBodyBuilder {}
}
#Value
#Builder
#JsonDeserialize(builder = ClientData.ClientDataBuilder.class)
class ClientData {
String something;
Integer somethingElse;
#JsonPOJOBuilder(withPrefix = "")
public static class ClientDataBuilder {}
}
This works as fine, as you'd expect, with a normal JSON payload e.g:
{
"id": "c29tZWlk",
"clientData": {
"something": "somethingValue",
"somethingElse": 1
}
}
However, we have a use-case where the clientData structure is known but, for reasons, is sent as a base64 encoded, stringified JSON blob e.g:
{
"id": "c29tZWlk",
"clientData": "eyJzb21ldGhpbmciOiJzb21ldGhpbmdWYWx1ZSIsInNvbWV0aGluZ0Vsc2UiOjF9"
}
It would be great if we could transparently decode and de-stringify this field as part of the deserialisation of PostBody before it calls runs the deserializer for ClientData.
One solution is create a custom deserialiser for PostBody, but in a real example there are a lot more fields that would then need to be handled manually.
I've tried creating a custom ClientData deserializer, but I'm struggling to understand the myriad of different types of desrializer interfaces available.
I've got something like this so far:
#Value
#Builder
#JsonDeserialize(builder = PostBody.PostBodyBuilder.class)
class PostBody {
byte[] id;
#JsonDeserialize(using = ClientDataBase64Deserializer.class)
ClientData clientData;
#JsonPOJOBuilder(withPrefix = "")
public static class PostBodyBuilder {}
}
// SNIP
class ClientDataBase64Deserializer extends StdScalarDeserializer<ClientData> {
protected ClientDataBase64Deserializer() {
super(ClientData.class);
}
#Override
public ClientData deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
byte[] value = Base64.getDecoder().decode(jsonParser.getText());
System.out.println(new String(value)); // prints stringified JSON
jsonParser.setCurrentValue(/* somehow convert stringified JSON to a Tree Node? */ value);
return deserializationContext.readValue(jsonParser, ClientData.class);
}
}
I'd be grateful for any ideas on how to progress with this example, or some other mechanism that I may be missing to solve this problem?
Cheers
In true SO fashion, I managed to solve my problem minutes after I asked the question.
This implementation of ClientDataBase64Deserializer and PostBody works as expected:
#Value
#Builder
#JsonDeserialize(builder = PostBody.PostBodyBuilder.class)
public class PostBody {
byte[] id;
ClientData clientData;
public interface IPostBodyBuilder {
#JsonDeserialize(using = ClientDataBase64Deserializer.class)
PostBody.PostBodyBuilder clientData(ClientData clientData);
}
#JsonPOJOBuilder(withPrefix = "")
public static class PostBodyBuilder implements IPostBodyBuilder {}
}
class ClientDataBase64Deserializer extends StdScalarDeserializer<ClientData> {
private final ObjectMapper objectMapper;
protected ClientDataBase64Deserializer(ObjectMapper objectMapper) {
super(ClientData.class);
this.objectMapper = objectMapper;
}
#Override
public ClientData deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) {
byte[] value = jsonParser.readValueAs(byte[].class);
return objectMapper.readValue(value, ClientData.class);
}
}
I have org.springframework.core.convert.converter.Converter
#Component
public class CatalogConverter implements Converter<ServiceCatalogType, Catalog> {
#Override
public Catalog convert(ServiceCatalogType source) {
Catalog catalog = new Catalog();
//convert
return catalog;
}
}
And I register this converter:
#Configuration
public class ConvertersConfig {
private final CatalogConverter catalogConverter;
#Autowired
public ConvertersConfig(CatalogConverter catalogConverter) {
this.catalogConverter = catalogConverter;
}
#Bean(name="conversionService")
ConversionService conversionService() {
ConversionServiceFactoryBean factoryBean = new ConversionServiceFactoryBean();
HashSet<Converter> converters = new HashSet<>();
converters.add(catalogConverter);
factoryBean.setConverters(converters);
factoryBean.afterPropertiesSet();
return factoryBean.getObject();
}
}
But I need pass some parameter to my custom converter. I have some ways:
Pass it in constructor - but how can register this converter?
Use wrapper
class Wrapper{
private ServiceCatalogType catalog;
private String uuid;
}
and change converter like this:
implements Converter<ServiceCatalogType, Wrapper>
Or maybe Spring has another way?
EDIT
I need next.
in service:
pulic void send() {
ServiceCatalogType cs = getServiceCatalogType();//get from net
User user = getUser();//get from db
//convert cs+user to Catalog(all cs fields+ some user fields to catalog)
Catalog catalog = conversionService.convert(cs, user, Catalog.class);
}
EDIT2
Wrapper implementation:
#Data
#AllArgsConstructor
#NoArgsConstructor
public class CatalogWrapper {
private ServiceCatalogType serviceCatalogType;
private User user;
}
CatalogWrapper wrapper = new CatalogWrapper(getServiceCatalog(), getUser);
catalog = conversionService.convert(wrapper, Catalog.class);
I have trouble with deserialization JSON to some of classes ChildA, ChildB and etc. that implements Basic interface in following example.
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = InstagramUser.class, name = "ChildA")
})
public interface Basic {
getName();
getCount();
}
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonTypeName("ChildA")
public class ChildA implements Basic { ... }
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonTypeName("ChildB")
public class ChildB implements Basic { ... }
...
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonIgnoreProperties(ignoreUnknown = true)
public class Response<E extends Basic> {
#JsonProperty("data")
private List<E> data;
public List<E> getData() {
return data;
}
public void setData(List<E> data) {
this.data = data;
}
}
// deserialization
HTTPClient.objectMapper.readValue(
response,
(Class<Response<ChildA>>)(Class<?>) Response.class
)
Exception is: com.fasterxml.jackson.databind.JsonMappingException: Unexpected token (END_OBJECT), expected FIELD_NAME: missing property 'type' that is to contain type id (for class Basic)
Expected JSON is like this:
{
"data": [{ ... }, ...]
}
There is no property that is presented in all type objects so they are completely different. But as you can see on readValue line I know what is expected type. How to structure JsonTypeInfo and JsonSubTypes annotaions to deserialize JSON as expected class?
I kinda had the same problem as you, based in the reading here: Jackson Deserialize Abstract Classes I created my own solution, it basically consists of creating my own deserializer, the trick is to use/identify a specific property within JSON to know which instance type should be returned from deserialization, example is:
public interface Basic {
}
First Child:
public class ChildA implements Basic {
private String propertyUniqueForThisClass;
//constructor, getters and setters ommited
}
SecondChild:
public class ChildB implements Basic {
private String childBUniqueProperty;
//constructor, getters and setters ommited
}
The deserializer (BasicDeserializer.java) would be like:
public class BasicDeserializer extends StdDeserializer<Basic> {
public BasicDeserializer() {
this(null);
}
public BasicDeserializer(final Class<?> vc) {
super(vc);
}
#Override
public Basic deserialize(final JsonParser jsonParser,
final DeserializationContext deserializationContext)
throws IOException {
final JsonNode node = jsonParser.getCodec().readTree(jsonParser);
final ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec();
// look for propertyUniqueForThisClass property to ensure the message is of type ChildA
if (node.has("propertyUniqueForThisClass")) {
return mapper.treeToValue(node, ChildA.class);
// look for childBUniqueProperty property to ensure the message is of type ChildB
} else if (node.has("childBUniqueProperty")) {
return mapper.treeToValue(node, ChildB.class);
} else {
throw new UnsupportedOperationException(
"Not supported class type for Message implementation");
}
}
}
Finally, you'd have an utility class (BasicUtils.java):
private static final ObjectMapper MAPPER;
// following good software practices, utils can not have constructors
private BasicUtils() {}
static {
final SimpleModule module = new SimpleModule();
MAPPER = new ObjectMapper();
module.addDeserializer(Basic.class, new BasicDeserializer());
MAPPER.registerModule(module);
}
public static String buildJSONFromMessage(final Basic message)
throws JsonProcessingException {
return MAPPER.writeValueAsString(message);
}
public static Basic buildMessageFromJSON(final String jsonMessage)
throws IOException {
return MAPPER.readValue(jsonMessage, Basic.class);
}
For testing:
#Test
public void testJsonToChildA() throws IOException {
String message = "{\"propertyUniqueForThisClass\": \"ChildAValue\"}";
Basic basic = BasicUtils.buildMessageFromJSON(message);
assertNotNull(basic);
assertTrue(basic instanceof ChildA);
System.out.println(basic);
}
#Test
public void testJsonToChildB() throws IOException {
String message = "{\"childBUniqueProperty\": \"ChildBValue\"}";
Basic basic = BasicUtils.buildMessageFromJSON(message);
assertNotNull(basic);
assertTrue(basic instanceof ChildB);
System.out.println(basic);
}
The source code can be found on: https://github.com/darkstar-mx/jsondeserializer
I find not exactly solution but a workaround. I used custom response class ChildAResponse and passed it to ObjectMapper.readValue() method.
class ChildAResponse extends Response<ChildA> {}
// deserialization
HTTPClient.objectMapper.readValue(
response,
ChildAResponse.class
)
So JsonTypeInfo and JsonSubTypes annotations on the interface are no longer needed.