Parse JSON to Java records with fasterxml.jackson - java

Java records can not - by design - inherit from another object (see Why Java records do not support inheritance?). So I wonder what would be the best way to achieve the following.
Given my JSON data contains objects that have some common data + unique data. For example, type, width and height are in all shapes, but depending on the type, they can have additional fields:
{
"name": "testDrawing",
"shapes": [
{
"type": "shapeA",
"width": 100,
"height": 200,
"label": "test"
},
{
"type": "shapeB",
"width": 100,
"height": 200,
"length": 300
},
{
"type": "shapeC",
"width": 100,
"height": 200,
"url": "www.test.be",
"color": "#FF2233"
}
]
}
In "traditional" Java you would do this with
BaseShape with width and height
ShapeA extends BaseShape with label
ShapeB extends BaseShape with length
ShapeC extends BaseShape with URL and color
But I'm a bit stubborn and really would like to use records.
My solution now looks like this:
No BaseShape
The common fields are repeated in all records
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonInclude(JsonInclude.Include.NON_NULL)
public record Drawing(
#JsonProperty("name")
String name,
#JsonProperty("shapes")
#JsonDeserialize(using = TestDeserializer.class)
List<Object> shapes // I don't like the Objects here...
) {
}
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonInclude(JsonInclude.Include.NON_NULL)
public record ShapeA (
#JsonProperty("type") String type,
#JsonProperty("width") Integer width,
#JsonProperty("height") Integer height,
#JsonProperty("label") String label
) {
}
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonInclude(JsonInclude.Include.NON_NULL)
public record ShapeB(
#JsonProperty("type") String type,
#JsonProperty("width") Integer width,
#JsonProperty("height") Integer height,
#JsonProperty("length") Integer length
) {
}
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonInclude(JsonInclude.Include.NON_NULL)
public record ShapeC(
#JsonProperty("type") String type,
#JsonProperty("width") Integer width,
#JsonProperty("height") Integer height,
#JsonProperty("url") String url,
#JsonProperty("color") String color
) {
}
I don't like repeated code and it's a bad practice... But in the end I can get this loaded with this helper class:
public class TestDeserializer extends JsonDeserializer {
ObjectMapper mapper = new ObjectMapper();
#Override
public List<Object> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
List<Object> rt = new ArrayList<>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
if (node instanceof ArrayNode array) {
for (Iterator<JsonNode> it = array.elements(); it.hasNext(); ) {
JsonNode childNode = it.next();
rt.add(getShape(childNode));
}
} else {
rt.add(getShape(node));
}
return rt;
}
private Object getShape(JsonNode node) {
var type = node.get("type").asText();
switch (type) {
case "shapeA":
return mapper.convertValue(node, ShapeA.class);
case "shapeB":
return mapper.convertValue(node, ShapeB.class);
case "shapeC":
return mapper.convertValue(node, ShapeC.class);
default:
throw new IllegalArgumentException("Shape could not be parsed");
}
}
}
And this test proves to be working OK:
#Test
void fromJsonToJson() throws IOException, JSONException {
File f = new File(this.getClass().getResource("/test.json").getFile());
String jsonFromFile = Files.readString(f.toPath());
ObjectMapper mapper = new ObjectMapper();
Drawing drawing = mapper.readValue(jsonFromFile, Drawing.class);
String jsonFromObject = mapper.writeValueAsString(drawing);
System.out.println("Original:\n" + jsonFromFile.replace("\n", "").replace(" ", ""));
System.out.println("Generated:\n" + jsonFromObject);
assertAll(
//() -> assertEquals(jsonFromFile, jsonFromObject),
() -> assertEquals("testDrawing", drawing.name()),
() -> assertTrue(drawing.shapes().get(0) instanceof ShapeA),
() -> assertTrue(drawing.shapes().get(1) instanceof ShapeB),
() -> assertTrue(drawing.shapes().get(2) instanceof ShapeC)
);
}
What would be the best way to achieve this with the Jackson library and Java Records?
Extra sidenote: I will also need to be able to write back to JSON in the same format as the original.

Records cannot inherit because they are intended to be a solid contract, but they can implement an interface. So you can do something like this with JasonSubTypes with Jackson 2.12 or above:
Models
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonInclude(JsonInclude.Include.NON_NULL)
public record Drawing(
String name,
List<BaseShape> shapes
) { }
// added benefit of interface here is it reminds you to have the default fields
#JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
#JsonSubTypes({
#JsonSubTypes.Type(ShapeA.class),
#JsonSubTypes.Type(ShapeB.class),
#JsonSubTypes.Type(ShapeC.class)
})
public interface BaseShape {
Integer width();
Integer height();
}
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonInclude(JsonInclude.Include.NON_NULL)
public record ShapeA (
Integer width,
Integer height,
String label
) implements BaseShape { }
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonInclude(JsonInclude.Include.NON_NULL)
public record ShapeB(
Integer width,
Integer height,
Integer length
) implements BaseShape { }
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonInclude(JsonInclude.Include.NON_NULL)
public record ShapeC(
Integer width,
Integer height,
String url,
String color
) implements BaseShape { }
Test Class
#Slf4j
class DemoTest {
private ObjectMapper objectMapper = ObjectMapperBuilder.getObjectMapper();
#Test
void test() throws JsonProcessingException {
final String testString = objectMapper
.writerWithDefaultPrettyPrinter()
.writeValueAsString(
new Drawing(
"happy",
List.of(
new ShapeA(1, 1, "happyShape"),
new ShapeB(2, 2, 3),
new ShapeC(2, 2, "www.shape.com/shape", "blue"
)
)
)
);
log.info("From model to string {}", testString);
Drawing drawing = objectMapper.readValue(testString, Drawing.class);
log.info(
"Captured types {}",
drawing
.shapes()
.stream()
.map(s -> s.getClass().getName())
.collect(Collectors.toSet())
);
log.info(
"From string back to model then again to string {}",
objectMapper
.writerWithDefaultPrettyPrinter()
.writeValueAsString(drawing)
);
}
}
Here's the test log output:
17:06:41.293 [Test worker] INFO com.demo.DemoTest - From model to string {
"name" : "happy",
"shapes" : [ {
"width" : 1,
"height" : 1,
"label" : "happyShape"
}, {
"width" : 2,
"height" : 2,
"length" : 3
}, {
"width" : 2,
"height" : 2,
"url" : "www.shape.com/shape",
"color" : "blue"
} ]
}
17:06:41.353 [Test worker] INFO com.demo.DemoTest - Captured types [com.demo.DemoTest$ShapeB, com.demo.DemoTest$ShapeA, com.demo.DemoTest$ShapeC]
17:06:41.354 [Test worker] INFO com.demo.DemoTest - From string back to model then again to string {
"name" : "happy",
"shapes" : [ {
"width" : 1,
"height" : 1,
"label" : "happyShape"
}, {
"width" : 2,
"height" : 2,
"length" : 3
}, {
"width" : 2,
"height" : 2,
"url" : "www.shape.com/shape",
"color" : "blue"
} ]
}
Note that you can add the type field as a name property of the #JsonSubTypes.Type annotation, but this works with or without discriminator as long as the fields in your records are never exactly the same.
You can read more about JsonSubtypes here.

Related

Creating the Base Entity class in relational entity classes

The json below is what was sent to me, when I findByPinCode. I have created relations between them in the following accordance with the json answer. Here are the relationships.
{
"RequestIdentifier": "00000000-0000-0000-0000-000000000000",
"Status": {
"Name": "string",
"Code": 0,
"Message": "string"
},
"Response": {
"Person": {
"Pin": "string",
"LastName": "string",
"FirstName": "string",
"FatherName": "string",
"BirthDate": "string"
},
"Company": {
"Voen": "string",
"Name": "string",
"PersonType": "string",
"Persons": [
{
"Pin": "string",
"LastName": "string",
"FirstName": "string",
"FatherName": "string",
"BirthDate": "string"
}
]
},
"Farms": [
{
"Id": 0,
"Name": "string",
"Fields": [
{
"VillageName": "string",
"DocType": "string",
"FieldAmount": 0,
"Unit": "string",
"Plants": [
{
"Name": "string",
"FieldAmount": 0,
"Unit": "string"
}
]
}
],
"Animals": [
{
"Sort": "string",
"Count": 0
}
],
"Bees": [
{
"Sort": "string",
"Count": 0
}
]
}
]
}
}
(One)Company To (Many)Persons
(One)FarmInfo To (Many)FarmFields
(One)FarmFields To (Many)Plants
(One)FarmInfo To (Many)Animals
(One)FarmInfo To (Many)Bees
and thus converting them to my dto class first. Then I want to convert them to my entity classes and persist them completely to the database.
This is my DTO class.
#Data
#NoArgsConstructor
#AllArgsConstructor
public class FarmInfoMainResult {
#JsonProperty(value = "RequestIdentifier")
private String requestIdentifier;
#JsonProperty(value = "Status")
private ResponseStatus status;
#JsonProperty(value = "Response")
private FarmInfoData response;
}
and this is my business logic layer of method, which I want to that in here convertint the dto class to the BaseEntity class and at the end of I want to persist BaseEntity class to the database.
#Override
public ResponseEntity<FarmInfoMainResult> findFarmInfoByPin(String pin, String username, String branchCode) {
String requestIdentifier = UUID.randomUUID().toString();
ResponseEntity<FarmInfoMainResult> farmInfoPinServiceResponse = clientUtility.getForObject(farmInfoForPinUrl + pin, requestIdentifier, FarmInfoMainResult.class);
System.out.println("farmInfoPinServiceResponse " + farmInfoPinServiceResponse.getBody());
/*
Here are I want to convert farmInfoPinServiceResponse variable to the my Base Entity class and so persist it to the database, so that
thus, values will be automatically added to them as they are related to each other in other relational databases.
*/
return farmInfoPinServiceResponse;
}
#Entity
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String requestIdentifier;
#OneToOne(cascade = CascadeType.ALL)
private Status status;
#OneToOne(cascade = CascadeType.ALL)
private Response response;
}
But I am getting the following error in my Response class. basic' attribute type should not be a container
#Entity
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class Response {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private PersonFarmInfo personFarmInfo;
private CompanyFarmInfo companyFarmInfo;
private FarmInfo farmInfo;
private FarmFieldInfo farmFieldInfo;
private FarmPlantInfo farmPlantInfo;
private FarmAnimalInfo farmAnimalInfo;
private FarmBeeInfo farmBeeInfo;
#OneToOne(mappedBy = "response")
private BaseEntity baseEntity;
}
If I don't create main entity class, there will be a situation like this. I will add the properties of each class that is in the dto to the properties of each of my entity class and print them to the database. But by insert to a single entity class I want others to be affected as well. I wanted to solve the issue by creating a main entity class like this, but I'm getting the following error.
As I said at the beginning, the codes below do this by separating them into parts. These codes my old version codes.
#Override
public ResponseEntity<FarmInfoMainResult> findFarmInfoByPin(String pin, String username, String branchCode) {
String requestIdentifier = UUID.randomUUID().toString();
ResponseEntity<FarmInfoMainResult> farmInfoPinServiceResponse = clientUtility.getForObject(farmInfoForPinUrl + pin, requestIdentifier, FarmInfoMainResult.class);
System.out.println("farmInfoPinServiceResponse " + farmInfoPinServiceResponse.getBody());
saveFarmInfoPin(farmInfoPinServiceResponse.getBody(), username, branchCode, pin);
return farmInfoPinServiceResponse;
}
Here I am sending the object stored in dto to my saveFarmInfoPin method.Then this method works. Here, my method saves all my object values stored in dto separately.
private void saveFarmInfoPin(FarmInfoMainResult farmInfoMainResult, String username, String branchcode, String pin) {
// try {
if (farmInfoMainResult.getResponse() != null) {
saveFarmInfoPersonPin(farmInfoMainResult.getResponse().getPerson(), farmInfoMainResult.getRequestIdentifier(), username, branchcode, pin);
saveFarmInfoCompanyPin(farmInfoMainResult.getResponse().getCompany(), farmInfoMainResult.getRequestIdentifier(), username, branchcode);
saveFarminfo(farmInfoMainResult.getResponse().getFarms());
/* } catch (Exception e) {
e.printStackTrace();
}
*/
}
}
Finally, my following methods divide each of my dto objects into parts, first converting them to my entity class, and then adding them to the database via repository.
private void saveFarmInfoPersonPin(FarmInfoPerson farmInfoPersonDto, String requestIdentifier, String username, String branchCode, String pin) {
if (farmInfoPersonDto != null) {
FarmPersonInfo newFarmInfoPerson = new FarmPersonInfo();
newFarmInfoPerson.setRequestIdentifier(requestIdentifier);
newFarmInfoPerson.setBranchCode(branchCode);
newFarmInfoPerson.setUsername(username);
newFarmInfoPerson.setPin(pin);
newFarmInfoPerson.setPin(farmInfoPersonDto.getPin());
newFarmInfoPerson.setFatherName(farmInfoPersonDto.getFatherName());
newFarmInfoPerson.setFirstName(farmInfoPersonDto.getFirstName());
newFarmInfoPerson.setBirthDate(farmInfoPersonDto.getBirthDate());
personFarmInfoRepository.save(newFarmInfoPerson);
}
}
private void saveFarmInfoCompanyPin(FarmInfoCompany farmInfoCompany, String requestIdentifier, String username, String branchCode) {
List<FarmPersonInfo> newFarmInfoPersonList = new ArrayList<>();
if (farmInfoCompany != null) {
FarmCompanyInfo newCompanyFarmInfo = new FarmCompanyInfo();
newCompanyFarmInfo.setName(farmInfoCompany.getName());
newCompanyFarmInfo.setRequestIdentifier(requestIdentifier);
newCompanyFarmInfo.setUsername(username);
newCompanyFarmInfo.setBranchCode(branchCode);
newCompanyFarmInfo.setPersonType(farmInfoCompany.getPersonType());
newCompanyFarmInfo.setVoen(farmInfoCompany.getVoen());
// CompanyFarmInfo companyFarmInfo = companyFarmInfoRepository.save(newCompanyFarmInfo);
for (FarmInfoPerson farmInfoPerson : farmInfoCompany.getPersons()) {
FarmPersonInfo personInfo = new FarmPersonInfo();
personInfo.setPin(farmInfoPerson.getPin());
personInfo.setFatherName(farmInfoPerson.getFatherName());
personInfo.setFirstName(farmInfoPerson.getFirstName());
personInfo.setBirthDate(farmInfoPerson.getBirthDate());
personInfo.setFarmCompanyInfo(newCompanyFarmInfo);
newFarmInfoPersonList.add(personInfo);
}
personFarmInfoRepository.saveAll(newFarmInfoPersonList);
}
}
private void saveFarminfo(List<FarmInfoFarm> farmInfoFarms) {
List<FarmFieldInfo> newFarmFieldInfoList = new ArrayList<>();
List<FarmAnimalInfo> newFarmAnimalInfoList = new ArrayList<>();
List<FarmBeeInfo> newFarmBeeInfoList = new ArrayList<>();
List<FarmPlantInfo> newFarmPlantInfoList = new ArrayList<>();
for (FarmInfoFarm farmInfoFarm : farmInfoFarms) {
FarmInfo newFarmInfo = new FarmInfo();
newFarmInfo.setName(farmInfoFarm.getName());
newFarmInfo.setFarmInfoId(farmInfoFarm.getId());
FarmInfo farmInfo = farmInfoRepository.save(newFarmInfo);
for (FarmInfoField farmInfoField : farmInfoFarm.getFields()) {
FarmFieldInfo newFarmFieldInfo = new FarmFieldInfo();
newFarmFieldInfo.setVillageName(farmInfoField.getVillageName());
newFarmFieldInfo.setDocType(farmInfoField.getDocType());
newFarmFieldInfo.setFieldAmount(farmInfoField.getFieldAmount());
newFarmFieldInfo.setUnit(farmInfoField.getUnit());
newFarmFieldInfo.setFarmInfo(farmInfo);
newFarmFieldInfoList.add(newFarmFieldInfo);
for (FarmInfoPlant farmInfoPlant : farmInfoField.getPlants()) {
FarmPlantInfo newfarmPlantInfo = new FarmPlantInfo();
newfarmPlantInfo.setName(farmInfoPlant.getName());
newfarmPlantInfo.setFieldAmount(farmInfoPlant.getFieldAmount());
newfarmPlantInfo.setUnit(farmInfoPlant.getUnit());
newfarmPlantInfo.setFarmFieldInfo(newFarmFieldInfo);
newFarmPlantInfoList.add(newfarmPlantInfo);
}
farmPlantInfoRepository.saveAll(newFarmPlantInfoList);
}
farmFieldInfoRepository.saveAll(newFarmFieldInfoList);
for (FarmInfoAnimal farmInfoAnimal : farmInfoFarm.getAnimals()) {
FarmAnimalInfo newFarmAnimalInfo = new FarmAnimalInfo();
newFarmAnimalInfo.setCount(farmInfoAnimal.getCount());
newFarmAnimalInfo.setSort(farmInfoAnimal.getSort());
newFarmAnimalInfo.setFarmInfo(farmInfo);
newFarmAnimalInfoList.add(newFarmAnimalInfo);
}
farmAnimalInfoRepository.saveAll(newFarmAnimalInfoList);
for (FarmInfoBee farmInfoBee : farmInfoFarm.getBees()) {
FarmBeeInfo newfarmBeeInfo = new FarmBeeInfo();
newfarmBeeInfo.setCount(farmInfoBee.getCount());
newfarmBeeInfo.setSort(farmInfoBee.getSort());
newfarmBeeInfo.setFarmInfo(farmInfo);
newFarmBeeInfoList.add(newfarmBeeInfo);
}
farmBeeInfoRepository.saveAll(newFarmBeeInfoList);
}
}
But as I said, instead of separating them all into separate-parts, can I add all of these objects to my main entity class and bring all these particles together through just one method?
I'm thinking of something like this, now I will use jackson to convert all the values in the json value I have received to the classes it belongs to, then I will add them to my main class and finally add my main class to my database. Since the main class will already have all the relations in itself, I think that all of them will be affected by doing this operation once.

Recommendation for resetting data in json object using Java

I need a recommendation for this situation.
I have a json object in string format that will have pattern like this:
{
"productCard" : {
"productA" : {
"state" : "Y",
"desc" : "AAA",
"someProp" : 112
},
"productB" : {
"state" : "X",
"desc" : " BBB ",
"listSomeThing" : [
{
"p1" : 1,
"p2" : "2"
},
{
"p2" : "3"
}
]
}
// PRODUCT CAN ADD MORE IN FUTRE
// ALSO CAN HAVE OTHER OBJECT TYPE
}
// THIS CAN HAVE OTHER OBJECT THAT MAY BE NON RELATE INFORMATION WITH PRODUCT CARD
}
and then this will be parsed to an object like this:
class Product {
protected String state
protected String desc
}
class SomeThing {
private int p1
private String p2
}
class ProductA extend Product {
private int someProp
}
class ProductB extend Product {
private List<SomeThing> listSomeThing
}
class ProductCard {
private ProductA prodctA
private ProductB productB
}
class BaseObject {
private ProductCard productCard
}
If I need to reset some field value in each product, and then parse to string format again, should I:
(1) create a new function in Product and then override in some child class for extra method:
class Product {
void reset(){
this.state = "X"
this.desc = ""
}
}
class productB extend Product {
#override
void reset(){
super.reset()
this.listSomeThing = new ArrayList<>()
}
}
and in base object create new function:
class ProductCard {
private ProductA productA
private ProductB productB
void resetAllProduct(){
this.productA.reset()
this.productB.reset()
}
}
class BaseObject {
private ProductCard productCard
void resetAllProductCard(){
this.productCard.resetAllProduct()
}
}
then call BaseObject.resetAllProductCard() where business needs to reset?
(2) create new function in business class? Or some util class:
void reset(ProdctCard productCard){
ProductA productA = productCard.getProductA();
productA.setState("X")
productA.setDesc("")
ProductB productB = productCard.getProdctB();
productB.setState("X")
productB.setDesc("")
productB.setListSomeThing(new ArrayList<>())
}
(3) another approach?
I would use Jackson Project for that job:
public String reset(String json) throws IOException {
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(json);
JsonNode productCardNode = jsonNode.get("productCard");
productCardNode.forEach(node -> ((ObjectNode) node).put("state", "X").put("desc", ""));
ObjectNode productBNode = (ObjectNode) productCardNode.get("productB");
productBNode.putArray("listSomeThing");
return jsonNode.toPrettyString();
}
Then:
String jsonReseted = reset(json);
System.out.println(jsonReseted);
Output:
{
"productCard" : {
"productA" : {
"state" : "X",
"desc" : "",
"someProp" : 112
},
"productB" : {
"state" : "X",
"desc" : "",
"listSomeThing" : [ ]
}
}
}

How to persist Json Object with nested arrays (including Json objects) to sql database in one table?

I write app in Spring.
This is my json: (it is an array of json objects)
[{"id" : 643419352,
"status" : "removed_by_user",
"url" : "https://www.olx.pl/d/oferta/opona-12-1-2-x-2-1-4-etrto-62-203-detka-CID767-IDHxILu.html",
"created_at" : "2020-11-27 10:46:07",
"activated_at" : "2020-12-11 12:41:12",
"valid_to" : "2020-12-17 15:38:10",
"title" : "opona 12 1/2 \" x 2 1/4 etrto 62-203 + dętka",
"description" : "opona w bardzo dobrym stanie + dętka, rozmiar 12 1/2 x 2 1/4 , dętka z zaworem samochodowym",
"category_id" : 1655,
"advertiser_type" : "private",
"external_id" : null,
"external_url" : null,
"contact" : {
"name" : "Damazy",
"phone" : "501474399"
},
"location" : {
"city_id" : 10609,
"district_id" : 301,
"latitude" : "51.80178",
"longitude" : "19.43928"
},
"images" : [ {
"url" : "https://ireland.apollo.olxcdn.com:443/v1/files/efa9any4ryrb-PL/image;s=1000x700"
} ],
"price" : {
"value" : "9",
"currency" : "PLN",
"negotiable" : false,
"budget" : false,
"trade" : false
},
"salary" : null,
"attributes" : [ {
"code" : "state",
"value" : "used",
"values" : null
} ],
"courier" : null
}, {
"id" : 643435839,
"status" : "removed_by_user",
"url" : "https://www.olx.pl/d/oferta/opona-4-80-4-00-8-do-taczki-nowa-CID628-IDHxN3p.html",
"created_at" : "2020-11-27 11:53:47",
"activated_at" : "2020-11-27 11:54:36",
"valid_to" : "2020-12-17 15:38:07",
"title" : "opona 4.80/4.00 - 8 do taczki nowa!!!",
"description" : "opona do taczki, nowa, nigdy nie używana, stan idealny.\r\nrozmiar 4.80/4.00-8. \r\nopona do taczki, nowa, nigdy nie używana, stan idealny.\r\nrozmiar 4.80/4.00-8.",
"category_id" : 1636,
"advertiser_type" : "private",
"external_id" : null,
"external_url" : null,
"contact" : {
"name" : "Damazy",
"phone" : "501474399"
},
"location" : {
"city_id" : 10609,
"district_id" : 301,
"latitude" : "51.80178",
"longitude" : "19.43928"
},
"images" : [ {
"url" : "https://ireland.apollo.olxcdn.com:443/v1/files/qmvssagjnq1r2-PL/image;s=1000x700"
} ],
"price" : {
"value" : "9",
"currency" : "PLN",
"negotiable" : false,
"budget" : false,
"trade" : false
},
"salary" : null,
"attributes" : [ {
"code" : "state",
"value" : "new",
"values" : null
} ],
"courier" : null
}]
and this is my entity class Advert :
#Setter
#Getter
#NoArgsConstructor
#AllArgsConstructor
#ToString
#Entity
public class Advert {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long ident;
private int id;
private String status;
private String url;
private String created_at;
private String activated_at;
private String valid_to;
private String title;
#Lob
private String description;
private int category_id;
private String advertiser_type;
private Long external_id;
private String external_url;
private String salary;
private String attributes;
private String courier;
#Embedded
private Location location;
#Embedded
private Contact contact;
#Embedded
private Price price;
private String images;
and my saveAdverts method :
#RequestMapping("/saveadverts")
public String saveAdverts() throws IOException {
HttpEntity<String> requestEntity = entity.requestEntityProvider();
String url = "https://www.olx.pl/api/partner/adverts";
ResponseEntity<JsonNode> responseEntity = template.exchange(url, HttpMethod.GET, requestEntity, JsonNode.class);
String adverts = responseEntity.getBody().get("data").toString();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
try {
Advert[] array = objectMapper.readValue(adverts, Advert[].class);
for(Advert a : array) {
advertRepository.save(a);
}
} catch (Exception e) {
System.out.println(e);
}
return "index";
}
what I want to do is to parse json to entity objects and save all adverts objects to sql database in one table.
Method execution stops with exception on this line :
Advert[] array = objectMapper.readValue(adverts, Advert[].class);
I get this error message :
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type java.lang.String from Array value (token JsonToken.START_ARRAY)
at [Source: (StringReader); line: 24, column: 14] (through reference chain: java.lang.Object[][0]->pl.vida.model.Advert["images"])
Please notice that the field "images" realtes to nested array of json objects.
Please help, I spent one week on this and no result. Thanks
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type java.lang.String from Array value (token JsonToken.START_ARRAY) at [Source: (StringReader); line: 24, column: 14] (through reference chain: java.lang.Object[][0]->pl.vida.model.Advert["images"])
You are getting the above exception because you are trying to convert an array of objects into a String which is not possible. See in your JSON images & attributes are array of objects.
"images": [{ "url": "https://ireland.apollo.olxcdn.com:443/v1/files/qmvssagjnq1r2-PL/image;s=1000x700" }],
"attributes": [{ "code": "state", "value": "new", "values": null }]
and in your Advert class you have created images & attributes as String types.
private String attributes;
private String images;
Generally, for array kind of object, we make fields either List or Set and if the field is List/Set then we need to create separate classes for them and map as OneToMany relationship. So creating separate classes means separate tables will be created but you don't want to have multiple tables. You want to store all the data in a single table. In a normal case, it is not possible but if we write some additional configuration classes then we can achieve your requirement. These tweaks have been provided by Hibernate itself.
So basically, Hibernate has provided some built-in types like String, Integer, Float, Date, Timezone, etc. Here you can check the complete list of built-in types. But according to our requirements, we can create custom types as well. So to store the array kind of data Hibernate didn't provide any built-in type. Hence we shall create a custom type.
Solution:
So we want to store an array of object data and we can easily store it in com.fasterxml.jackson.databind.JsonNode object. But Hibernate doesn't support this class as a field type. Hence to make supportable for this class we need to write 2 extra classes i.e. JsonNodeStringType.java & JsonNodeStringDescriptor.
JsonNodeStringType.java
public class JsonNodeStringType extends AbstractSingleColumnStandardBasicType<JsonNode> implements DiscriminatorType<JsonNode> {
public static final JsonNodeStringType INSTANCE = new JsonNodeStringType();
public JsonNodeStringType() {
super(VarcharTypeDescriptor.INSTANCE, JsonNodeStringDescriptor.INSTANCE);
}
#Override
public String getName() {
return "JsonNode";
}
#Override
public JsonNode stringToObject(String xml) {
return fromString(xml);
}
#Override
public String objectToSQLString(JsonNode value, Dialect dialect) {
return '\'' + toString(value) + '\'';
}
}
JsonNodeStringDescriptor.java
public class JsonNodeStringDescriptor extends AbstractTypeDescriptor<JsonNode> {
public static final ObjectMapper mapper = new ObjectMapper();
public static final JsonNodeStringDescriptor INSTANCE = new JsonNodeStringDescriptor();
public JsonNodeStringDescriptor() {
super(JsonNode.class, ImmutableMutabilityPlan.INSTANCE);
}
#Override
public String toString(JsonNode value) {
try {
return mapper.writeValueAsString(value);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
#Override
public JsonNode fromString(String string) {
try {
return mapper.readTree(string);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
#Override
public <X> X unwrap(JsonNode value, Class<X> type, WrapperOptions options) {
if (value == null) {
return null;
}
if (String.class.isAssignableFrom(type)) {
return (X) toString(value);
}
throw unknownUnwrap(type);
}
#Override
public <X> JsonNode wrap(X value, WrapperOptions options) {
if (value == null) {
return null;
}
if (String.class.isInstance(value)) {
return fromString(value.toString());
}
throw unknownWrap(value.getClass());
}
}
Now our Advert class will look like as
import org.hibernate.annotations.Type;
#Setter
#Getter
#ToString
#Entity
#Table(name = "advert")
#TypeDef(name = "JsonNode", typeClass = JsonNodeStringType.class)
public class Advert {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "ident", unique = true, nullable = false)
private Long ident;
private int id;
private String status;
private String url;
private String created_at;
private String activated_at;
private String valid_to;
private String title;
#Lob
private String description;
private int category_id;
private String advertiser_type;
private Long external_id;
private String external_url;
private String salary;
private String courier;
#Embedded
private Location location;
#Embedded
private Contact contact;
#Embedded
private Price price;
#Type(type = "JsonNode")
private JsonNode images;
#Type(type = "JsonNode")
private JsonNode attributes;
}
Here we go. If you execute the below code it works perfectly.
String advertsString = "[ { \"id\": 643419352, \"status\": \"removed_by_user\", \"url\": \"https://www.olx.pl/d/oferta/opona-12-1-2-x-2-1-4-etrto-62-203-detka-CID767-IDHxILu.html\", \"created_at\": \"2020-11-27 10:46:07\", \"activated_at\": \"2020-12-11 12:41:12\", \"valid_to\": \"2020-12-17 15:38:10\", \"title\": \"opona 12 1/2 \\\" x 2 1/4 etrto 62-203 + dętka\", \"description\": \"opona w bardzo dobrym stanie + dętka, rozmiar 12 1/2 x 2 1/4 , dętka z zaworem samochodowym\", \"category_id\": 1655, \"advertiser_type\": \"private\", \"external_id\": null, \"external_url\": null, \"contact\": { \"name\": \"Damazy\", \"phone\": \"501474399\" }, \"location\": { \"city_id\": 10609, \"district_id\": 301, \"latitude\": \"51.80178\", \"longitude\": \"19.43928\" }, \"images\": [ { \"url\": \"https://ireland.apollo.olxcdn.com:443/v1/files/efa9any4ryrb-PL/image;s=1000x700\" } ], \"price\": { \"value\": \"9\", \"currency\": \"PLN\", \"negotiable\": false, \"budget\": false, \"trade\": false }, \"salary\": null, \"attributes\": [ { \"code\": \"state\", \"value\": \"used\", \"values\": null } ], \"courier\": null }, { \"id\": 643435839, \"status\": \"removed_by_user\", \"url\": \"https://www.olx.pl/d/oferta/opona-4-80-4-00-8-do-taczki-nowa-CID628-IDHxN3p.html\", \"created_at\": \"2020-11-27 11:53:47\", \"activated_at\": \"2020-11-27 11:54:36\", \"valid_to\": \"2020-12-17 15:38:07\", \"title\": \"opona 4.80/4.00 - 8 do taczki nowa!!!\", \"description\": \"opona do taczki, nowa, nigdy nie używana, stan idealny.\\r\\nrozmiar 4.80/4.00-8. \\r\\nopona do taczki, nowa, nigdy nie używana, stan idealny.\\r\\nrozmiar 4.80/4.00-8.\", \"category_id\": 1636, \"advertiser_type\": \"private\", \"external_id\": null, \"external_url\": null, \"contact\": { \"name\": \"Damazy\", \"phone\": \"501474399\" }, \"location\": { \"city_id\": 10609, \"district_id\": 301, \"latitude\": \"51.80178\", \"longitude\": \"19.43928\" }, \"images\": [ { \"url\": \"https://ireland.apollo.olxcdn.com:443/v1/files/qmvssagjnq1r2-PL/image;s=1000x700\" } ], \"price\": { \"value\": \"9\", \"currency\": \"PLN\", \"negotiable\": false, \"budget\": false, \"trade\": false }, \"salary\": null, \"attributes\": [ { \"code\": \"state\", \"value\": \"new\", \"values\": null } ], \"courier\": null } ]";
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Advert[] adverts = objectMapper.readValue(advertsString, Advert[].class);
for (Advert advert : adverts) {
Advert saved = advertRepository.save(advert);
System.out.println("saved " + saved.getIdent());
}
I hope your problem gets resolved which you have been stuck with for one week. If you don't want to manually create these types of descriptors you can follow this article to use as an external dependency.

JsonView serialization that depends on some condition/property name etc

One of my classes has 3 properties of same type. Now I'm trying to serialize it do JSON, but one of those properties needs to be serialized differently - basically one of those properties is "internal" and I need only id of it, the rest of them must be fully serialized.
What I came so far:
#NoArgsConstructor #AllArgsConstructor #Data
public static class Id {
#JsonView(View.IdOnly.class) private long id;
}
#NoArgsConstructor #AllArgsConstructor #Data
public static class Company extends Id {
#JsonView(View.Tx.class) private String name;
#JsonView(View.Tx.class) private String address;
}
#NoArgsConstructor #AllArgsConstructor #Data
public static class Transaction {
#JsonView(View.Tx.class) private Company from;
#JsonView(View.Tx.class) private Company to;
#JsonView(View.IdOnly.class) private Company createdBy;
}
public static class View {
public interface Tx extends IdOnly {}
public interface IdOnly {}
}
And quick test for it:
#Test
void test() throws JsonProcessingException {
Company s = new Company("Source", "address_from");
Company d = new Company("Destination", "address_to");
final Transaction t = new Transaction(s, d, s);
final ObjectMapper m = new ObjectMapper();
System.out.println(m.writerWithDefaultPrettyPrinter().withView(View.Tx.class).writeValueAsString(t));
}
And output is:
{
"from" : {
"id" : 0,
"name" : "Source",
"address" : "address_from"
},
"to" : {
"id" : 0,
"name" : "Destination",
"address" : "address_to"
},
"createdBy" : {
"id" : 0,
"name" : "Source",
"address" : "address_from"
}
}
Now, question, how can I customize serialization of createBy property? I need following output:
{
"from" : {
"id" : 0,
"name" : "Source",
"address" : "address_from"
},
"to" : {
"id" : 0,
"name" : "Destination",
"address" : "address_to"
},
"createdBy" : {
"id" : 0,
}
}
Oh, I think that answer for that is very simple:
Mark createdBy field with #JsonSerialize(using = CS.class)
Implement custom serializer as follows:
public static class CS extends JsonSerializer<Company> {
#Override
public void serialize(Company company, JsonGenerator jgen, SerializerProvider serializerProvider) throws IOException {
jgen.writeStartObject();
jgen.writeNumberField("id", company.getId());
jgen.writeEndObject();
}
}

Convert XML key value mapping to JSON object

I currently have an object which is a key-value pair that I have converted from XSD to POJO using JAXB and I tried using Jackson 2.x to get the JSON output for the POJO. This JSON output looks like:
[ {
"key" : "key1",
"value" : 1
}, {
"key" : "key2",
"value" : "2"
}, {
"key" : "key3",
"value" : [ ]
} ]
Currently my XSD generated POJO looks like:
#XmlAccessorType(XmlAccessType.FIELD)
#XmlType(name = "MapEntryType", propOrder = {
"value"
})
public class MapEntryType {
#XmlElement(required = true)
protected Object value;
#XmlAttribute(name = "key", required = true)
protected String key;
}
#XmlAccessorType(XmlAccessType.FIELD)
#XmlType(name = "", propOrder = {
"mapEntries"
})
#XmlRootElement(name = "EventsSearchResponse")
public class EventsSearchResponse {
#XmlElement(name = "MapEntry")
protected List<MapEntryType> mapEntries;
}
I would like to generate the map as a simple JSON object:
{ "key1" : 1, "key2" : "2", "key3" : []}
I went over the annotations that are available in Jackson from http://wiki.fasterxml.com/JacksonAnnotations but I have not been able to find a way to perform this type of conversion. Any help regarding this would be really appreciated! Thanks.
I was able to achieve the desired results with a custom serializer (written as inner class for convinience):
public static class EventsSearchResponseSerializer extends JsonSerializer<EventsSearchResponse>
{
#Override
public void serialize(EventsSearchResponse res, JsonGenerator gen, SerializerProvider serializers)
throws IOException, JsonProcessingException
{
gen.writeStartObject();
for (MapEntryType t : res.mapEntries) {
gen.writeObjectField(t.key, t.value);
}
gen.writeEndObject();
}
}
added the proper annotation to the POJO:
#JsonSerialize(using = EventsSearchResponseSerializer.class)
#XmlRootElement(name = "EventsSearchResponse")
public static class EventsSearchResponse {
#XmlElement(name = "MapEntry")
public List<MapEntryType> mapEntries;
}
calling the Jackson mapper:
public static void main(String[] args)
{
EventsSearchResponse r = new EventsSearchResponse();
r.mapEntries = new ArrayList<>();
MapEntryType t = new MapEntryType();
t.key = "key1";
t.value = new Integer(1);
r.mapEntries.add(t);
t = new MapEntryType();
t.key = "key2";
t.value = new Integer(2);
r.mapEntries.add(t);
t = new MapEntryType();
t.key = "key2";
t.value = new String[0];
r.mapEntries.add(t);
try {
System.out.println(new ObjectMapper().writeValueAsString(r));
} catch (Exception e) {
e.printStackTrace();
}
}
gives result:
{"key1":1,"key2":2,"key2":[]}

Categories