I have a rather complex JSON file which I need to parse into multiple Java classes.
The structure of JSON is as follows:
{
"dataBlock": [
{
"headingName": {
"name": "Operational Information",
"position": "1",
"attributes": [
{
"name": "Brand",
"position": "1",
"value": [
"A",
"B"
]
},
{
"name": "Data Model Id",
"position": "2",
"value": "000001"
}
]
}
},
{
"headingName": {
"name": "CRA",
"position": "6",
"attributes": [
{
"name": "Company",
"position": "1",
"value": "private_limited_company"
},
{
"name": "Address",
"position": "3",
"value": {
"line1": "AAA",
"line2": "BBB",
"line3": "CCC",
"line4": "DDD",
"postalCode": "AB XYZ",
"countryCode": "GBR"
}
}
]
}
}
]
}
I wish to store each "headingName" node into a separate Java Class (the structure may vary in future for individual categories/headingName within dataBlock Array such as my response is:
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class MyResponse {
private OperationalInformation operationalInformation;
private CRA cra;
...
}
How do I achieve that?
CodePudding user response:
My Solution involves two stages:
deserialization
deserialize the json into objects in the same hierarchy and structure of the input json that contain String and object properties. This set of classes can support any and all heading names and attributes
@Data
public abstract class JsonCommonProperties {
protected String name;
protected String position;
}
@Data
@ToString(callSuper = true)
public class JsonAttribute<T> extends JsonCommonProperties {
private T value;
}
@Data
@ToString(callSuper = true)
public class JsonHeading extends JsonCommonProperties {
private List<JsonAttribute<?>> attributes;
}
public class JsonResponse {
public List<HeadingName> dataBlock;
public static class HeadingName {
public JsonHeading headingName;
}
}
this set of classes can be used for straightforward deserialization using ObjectMapper.readValue()
.
build MyResponse
convert the above object hierarchy to MyResponse, using reflection and 3rd party libraries to build objects and populate instance variables from String names.
I used one common super class for all Headings. the set of classes is specified below
@Getter
@Setter
@ToString
public abstract class Heading {
private String name;
public Heading(String name) {
setName(name);
}
}
@Getter
@Setter
@ToString(callSuper = true)
class CRA extends Heading {
private String company;
private Map<String, String> address;
public CRA (String name) {
super(name);
}
}
@Getter
@Setter
@ToString(callSuper = true)
class OperationalInformation extends Heading {
private List<String> brand;
private String dataModelId;
public OperationalInformation (String name) {
super(name);
}
}
@Data
public class MyResponse {
private OperationalInformation operationalInformation;
private CRA cra;
}
the challenges and solutions were
convert heading name to type of heading: I used apache common
StringUtils
and apache textCaseUtils
to convert json property to class name, and then used reflection to instantiate an instancepopulate instance vairables of headings from
JsonAttribute
s: I used Jodd Util library (better alternative to apacheBeanUtils
) to dynamically call setters from string names.
I put the above solutions in one heading factory class
public class HeadingFactory {
/**
* instantiate concrete Heading from specified JsonHeading
*/
@SuppressWarnings("unchecked")
public static Heading getHeading(JsonHeading jsonHeading) throws ReflectiveOperationException {
Class<Heading> concreteHeadingClass =
(Class<Heading>)Class.forName(getMyResponsePackage() "." getHeadingType(jsonHeading));
Heading heading = concreteHeadingClass.getConstructor(String.class).newInstance(jsonHeading.getName());
populateHeading(heading, jsonHeading);
return heading;
}
/**
* return class name of concrete Heading (type of instance var in MyResponse)
* from specified JsonHeading
*/
public static String getHeadingType(JsonHeading jsonHeading) {
// handle all uppercase heading, like CRA
if (StringUtils.isAllUpperCase(jsonHeading.getName())) return jsonHeading.getName();
// handle separate word heading, like Operational Information
return CaseUtils.toCamelCase(jsonHeading.getName(), true, ' ');
}
private static String getMyResponsePackage() {
return StringUtils.substringBeforeLast(MyResponse.class.getName(), ".");
}
// use jodd util to dynamically call setter methods of heading
// and pass values of jsonAttributes of jsonHeading
private static void populateHeading(Heading heading, JsonHeading jsonHeading) {
jsonHeading.getAttributes().forEach(jsonAttribute ->
BeanUtil.pojo.setProperty(heading, jsonAttribute.getPropertyName(), jsonAttribute.getValue())
);
}
}
populate instance variables of MyResponse with same technique and library. here's my main method
public static void main(String[] args) {
ObjectMapper mapper = new ObjectMapper();
try (BufferedReader br = Files.newBufferedReader(Paths.get("C://temp/json.json"))) {
JsonResponse jsonResponse = mapper.readValue(br, JsonResponse.class);
MyResponse myResponse = new MyResponse();
for (JsonResponse.HeadingName headingName : jsonResponse.dataBlock) {
Heading heading = HeadingFactory.getHeading(headingName.headingName);
String myResponseProperty = headingName.headingName.getPropertyName();
BeanUtil.pojo.setProperty(myResponse, myResponseProperty, heading);
}
System.out.println(myResponse);
} catch (Exception e) {
e.printStackTrace();
}
}
CodePudding user response:
Use JSON query language to transform the JSON structure before deserialize into POJO. A single Josson query statement can do the job.
dataBlock.map(headingName.name.camelCase()::headingName.field(name:)).mergeObjects()
https://github.com/octomix/josson
Demo
Josson josson = Josson.fromJsonString(
"{"
" \"dataBlock\": [ {"
" \"headingName\": {"
" \"name\": \"Operational Information\","
" \"position\": \"1\","
" \"attributes\": [ {"
" \"name\": \"Brand\","
" \"position\": \"1\","
" \"value\": [\"A\",\"B\"]"
" },"
" {"
" \"name\": \"Data Model Id\","
" \"position\": \"2\","
" \"value\": \"000001\""
" } ]"
" }"
" },"
" {"
" \"headingName\": {"
" \"name\": \"CRA\","
" \"position\": \"6\","
" \"attributes\": [ {"
" \"name\": \"Company\","
" \"position\": \"1\","
" \"value\": \"private_limited_company\""
" },"
" {"
" \"name\": \"Address\","
" \"position\": \"3\","
" \"value\": {"
" \"line1\": \"AAA\","
" \"line2\": \"BBB\","
" \"line3\": \"CCC\","
" \"line4\": \"DDD\","
" \"postalCode\": \"AB XYZ\","
" \"countryCode\": \"GBR\""
" }"
" } ]"
" }"
" } ]"
"}");
JsonNode node = josson.getNode(
"dataBlock.map(headingName.name.camelCase()::headingName.field(name:)).mergeObjects()");
System.out.println(node.toPrettyString());
Output
{
"operationalInformation" : {
"position" : "1",
"attributes" : [ {
"name" : "Brand",
"position" : "1",
"value" : [ "A", "B" ]
}, {
"name" : "Data Model Id",
"position" : "2",
"value" : "000001"
} ]
},
"cra" : {
"position" : "6",
"attributes" : [ {
"name" : "Company",
"position" : "1",
"value" : "private_limited_company"
}, {
"name" : "Address",
"position" : "3",
"value" : {
"line1" : "AAA",
"line2" : "BBB",
"line3" : "CCC",
"line4" : "DDD",
"postalCode" : "AB XYZ",
"countryCode" : "GBR"
}
} ]
}
}