Recommendation for resetting data in json object using Java - 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" : [ ]
}
}
}

Related

Parse JSON to Java records with fasterxml.jackson

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.

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();
}
}

Gson deserialization with changing field types

I get a JSON which has the following structure:
{"field1": "string",
"field2": false,
"a": {
"b": {
"listString": []
},
"c": {
"listString": [],
"s": "string"
},
"parent": {
"childA": {
"listString": ["string", "string"]
},
"s": "string"
},
"parent2": {
"listString": ["string", "string"],
"s": "string"
}
},
"field3": ["s", "s"]
}
I'm facing problems with the parent (and parent2) because the format of those fields can change. While the format of the complex objects b and c stays the same. For example, I can get parent (the same holds for parent2) in this way:
{"parent": {
"childA":{
"listString": ["ssssa", "a"]
},
"s": "string"
}}
or
{"parent": {
"listString": ["ssssa", "a"],
"s": "string"
}}
Moreover, childA field (if exists) can have different names, it can be childB or childC
I created java classes for the complex objects:
public class MyPojo{
private String[] field1;
private String field2;
private A a;
private String field3;...}
public class A{
private B b;
private C c;
private Parent parent;
private Parent2 parent2;..}
public class Parent{
private String s;
private ChildA childA;...}...
How can I deserialize something like this with Gson if the parent and parent2 objects have different formates?
This is the parent class:
public class Parent {
Map<String, JsonElement> parent = null;
public Map<String, JsonElement> getParent() {
return parent;
}
public void setParent(Map<String, JsonElement> parent) {
this.parent = parent;
}
}
This is the main class:
public static void main(String[] args) {
// TODO Auto-generated method stub
String input = "{\"parent\": {\"s\": \"string\",\"childA\":{\"listString\": [\"ssssa\", \"a\"]}}}";
Gson gsonInstance = null;
gsonInstance = new GsonBuilder().create();
Parent p = gsonInstance.fromJson(input, Parent.class);
Map<String, JsonElement> parentMap = p.getParent();
Set<String> keyMap = parentMap.keySet();
Iterator<String> iter = keyMap.iterator();
while(iter.hasNext()){
String name = iter.next();
if(name.matches("child(.*)")){
System.out.println(parentMap.get(name));
// do your logic
}
if (keyMap.contains("listString")){
List<String> listString = getListString(parentMap.get("listString"));
System.out.println(listString.toString());
}
}
}
public static List<String> getListString(JsonElement list){
Type listType = new TypeToken<List<String>>() {}.getType();
List<String> listString = new Gson().fromJson(list, listType);
return listString;
}
Hope it helps!

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":[]}

Order of JSON objects using Jackson's ObjectMapper

I'm using ObjectMapper to do my java-json mapping.
ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
ow.writeValue(new File( fileName +".json"), jsonObj);
this is my java class:
public class Relation {
private String id;
private String source;
private String target;
private String label;
private List<RelAttribute> attributes;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
public String getTarget() {
return target;
}
public void setTarget(String target) {
this.target = target;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public void setAttributes(List<RelAttribute> attributes) {
this.attributes = attributes;
}
public List<RelAttribute> getAttributes() {
return attributes;
}
}
this is what I get:
{
"id" : "-75da69d3-79c8-4000-a3d8-b10350a57a7e",
"attributes" : [ {
"attrName" : "ID",
"attrValue" : ""
}, {
"attrName" : "Description",
"attrValue" : "Primary Actor"
}, {
"attrName" : "Status",
"attrValue" : ""
} ],
"label" : "new Label",
"target" : "-46b238ac-b8b3-4230-b32c-be9707f8b691",
"source" : "-daa34638-061a-45e0-9f2e-35afd6c271e0"
}
So my question now is, how can I get this json output:
{
"id" : "-75da69d3-79c8-4000-a3d8-b10350a57a7e",
"label" : "new Label",
"target" : "-46b238ac-b8b3-4230-b32c-be9707f8b691",
"source" : "-daa34638-061a-45e0-9f2e-35afd6c271e0",
"attributes" : [ {
"attrName" : "ID",
"attrValue" : ""
}, {
"attrName" : "Description",
"attrValue" : "Primary Actor"
}, {
"attrName" : "Status",
"attrValue" : ""
} ]
}
I want it with same order as in my java declaration. Is there a way to specify it ? Maybe with annotations or stuff like that ?
#JsonPropertyOrder({ "id", "label", "target", "source", "attributes" })
public class Relation { ... }
Do you know there is a convenient way to specify alphabetic ordering?
#JsonPropertyOrder(alphabetic = true)
public class Relation { ... }
If you have specific requirements, here how you configure custom ordering:
#JsonPropertyOrder({ "id", "label", "target", "source", "attributes" })
public class Relation { ... }
The ordering of fields within a generated .class is indeterminate, so you can't count on that.
If you want specific ordering per class then you'll need to use the one of the approaches specified in other answers.
If you want everything to default to alphabetical ordering (e.g. for consistency in how the JSON is structured) then you can configure the ObjectMapper like this:
ObjectMapper mapper = new ObjectMapper();
mapper.setConfig(mapper.getSerializationConfig()
.with(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY));
For more consistent JSON consider also adding:
.with(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS)
One advantage of this approach is that you don't have to modify each class being serialized.
I discovered a third way today in case alphabetic is not your desired sorting order. It turns out adding a #JsonProperty annotation on a field places it last when writing. I discovered that when I wanted to specify a property name which did not conform to java naming conventions.
By Adding an index attribute you can define the order. Lowest index is placed first.
#JsonProperty(index=20)
String prop1;
#JsonProperty(index=10)
String prop2;
Would render:
{"prop2": "valueProp2", "prop1": "valueProp1"}
You can use #XmlAccessorType(XmlAccessType.FIELD)
#XmlType(name = "response", propOrder = { "prop1", "prop2",
"prop3", "prop4", "prop5", "prop6" }).
#JsonPropertyOrder requires a new jar to be added.
As per this documentation, you can configure Jackson2ObjectMapperBuilder globally. This class is available in spring-web dependency.
#Bean
public Jackson2ObjectMapperBuilder objectMapperBuilder() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.featuresToEnable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY);
return builder;
}
Also, you can use #JsonProperty(index) to determine the order in inherited classes as well.
class animal {
#JsonProperty(index=2)
int p1;
#JsonProperty(index=3)
int p2;
}
class cat extends animal{
#JsonProperty(index=1)
int p3;
}

Categories