I'm using Jackson in order to have log outputs as JSON.
The things is that we allow logging with the following syntax:
log.info("yourMessage {}", innerMessageObject, Meta.of("key", ObjectValue))
OUTPUT I HAVE
{
"level": INFO,
... classic logging attributes
"metadata": {
"object1": "value 1",
"object2": { ... }
...
}
}
OUTPUT I WANT
{
"level": INFO,
... classic logging attributes
"object1": "value 1",
"object2": { ... }
...
}
My log POJO
#Data
class JsonLog {
#JsonIgnore
private static final ObjectMapper mapper = JsonLog.initMapper();
private final String message;
private final String class_name;
private final Collection<Object> metadata;
private final Marker marker;
private final String level;
private final Long timestamp;
private final String thread;
private final String logger;
private final LoggerContextVO logger_context;
private final Map<String, String> environment_vars;
}
I don't succeed to only have the metadata attribute to be serialized as top level attributes.
It seems I cannot use #JsonUnwrapped because of this issue, I also tried this solution but cannot see how to implement it.
Have you got any ideas ?
Thank you :)
Would it be OK to convert metadata to be Map<String, Object>? It seems so by you example JSON and that's actually the natural generic representation of JSON in Java. In that case:
private final Map<String, Object> metadata;
#JsonAnyGetter
Map<String, Object> getMetadata() {
return metadata;
}
According to the docs, this annotation marks the getter method:
to be serialized as part of containing POJO (similar to unwrapping)
and can only be used with methods returning a Map. I am not aware of a solution for Collection
Related
I'm using MongoDb for unstructured documents. When I do the aggregations, I'm getting final output as unstructured objects. I post some sample data for the easiness. Actual objects have many fields.
Eg :
[
{ _id : "1", type: "VIDEO", videoUrl : "youtube.com/java"},
{ _id : "2", type: "DOCUMENT", documentUrl : "someurl.com/spring-boot-pdf"},
{ _id : "3", type: "ASSESSMENT", marks : 78}
]
The respective class for the types of above objects are
#Data
public class Video{
private String _id;
private String type;
private String videoUrl;
}
#Data
public class Document{
private String _id;
private String type;
private String documentUrl;
}
#Data
public class Assessment{
private String _id;
private String type;
private Integer marks;
}
Since I can't specify the converter class, I get all objects as list of Object.class which is a general type for all.
List<Object> list = mongoTemplate.aggregate(aggregation, mongoTemplate.getCollectionName(YOUR_COLLECTION.class), Object.class).getMappedResults();
It's working, but this is not readable and not maintainable for backend and front-end developers (eg : swagger ui). So I came up with a solution, that put all fields as a class.
#Data
#JsonInclude(JsonInclude.Include.NON_NULL)
class MyConvetor{
private String _id;
private String type;
private String videoUrl;
private String documentUrl;
private Integer marks;
}
Here Jackson helps to ignore all null fields
Now I can use MyConverter as Type
List<MyConverter> list = mongoTemplate.aggregate(aggregation, mongoTemplate.getCollectionName(YOUR_COLLECTION.class), MyConverter.class).getMappedResults();
But I feel this is not a good practice when we implementing a standard application. I'd like to know, is there any way to avoid the general type class (e.g. extending any abstract class)? Or is this the only way I can do?
I don't think so (or I don't know) if MongoDB in Java provides this kind of dynamic conversion by some field (it would require specify what field and what classes). But you can do it by hand.
First, you need to define your types (enum values or some map) for matching string to class. You can create abstract parent class (eg. TypedObject) for easier usage and binding all target classes (Video, Document, Assessment) .
Next you have to read and map values from Mongo to anything because you want to read all data in code. Object is good but I recommend Map<String, Object> (your Object actually is that Map - you can check it by invoking list.get(0).toString()). You can also map to String or DBObject or some JSON object - you have to read "type" field by hand and get all data from object.
At the end you can convert "bag of data" (Map<String, Object> in my example) to target class.
Now you can use converted objects by target classes. For proving these are actually target classes I print objects with toString all fields.
Example implementation
Classes:
#Data
public abstract class TypedObject {
private String _id;
private String type;
}
#Data
#ToString(callSuper = true)
public class Video extends TypedObject {
private String videoUrl;
}
#Data
#ToString(callSuper = true)
public class Document extends TypedObject {
private String documentUrl;
}
#Data
#ToString(callSuper = true)
public class Assessment extends TypedObject {
private Integer marks;
}
Enum for mapping string types to classes:
#RequiredArgsConstructor
public enum Type {
VIDEO("VIDEO", Video.class),
DOCUMENT("DOCUMENT", Document.class),
ASSESSMENT("ASSESSMENT", Assessment.class);
private final String typeName;
private final Class<? extends TypedObject> clazz;
public static Class<? extends TypedObject> getClazz(String typeName) {
return Arrays.stream(values())
.filter(type -> type.typeName.equals(typeName))
.findFirst()
.map(type -> type.clazz)
.orElseThrow(IllegalArgumentException::new);
}
}
Method for converting "bag of data" from JSON to your target class:
private static TypedObject toClazz(Map<String, Object> objectMap, ObjectMapper objectMapper) {
Class<? extends TypedObject> type = Type.getClazz(objectMap.get("type").toString());
return objectMapper.convertValue(objectMap, type);
}
Read JSON to "bags of data" and use of the above:
String json = "[\n" +
" { _id : \"1\", type: \"VIDEO\", videoUrl : \"youtube.com/java\"},\n" +
" { _id : \"2\", type: \"DOCUMENT\", documentUrl : \"someurl.com/spring-boot-pdf\"},\n" +
" { _id : \"3\", type: \"ASSESSMENT\", marks : 78}\n" +
"]";
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
List<Map<String, Object>> readObjects = objectMapper.readValue(json, new TypeReference<>() {});
for (Map<String, Object> readObject : readObjects) {
TypedObject convertedObject = toClazz(readObject, objectMapper);
System.out.println(convertedObject);
}
Remarks:
In example I use Jackson ObjectMapper for reading JSON. This makes the example and testing simpler. I think you can replace it with mongoTemplate.aggregate(). But anyway I need ObjectMapper in toClazz method for converting "bags of data".
I use Map<String, Object> instead of just Object. It is more complicated: List<Map<String, Object>> readObjects = objectMapper.readValue(json, new TypeReference<>() {});. If you want, you can do something like this: List<Object> readObjects2 = (List<Object>) objectMapper.readValue(json, new TypeReference<List<Object>>() {});
Result:
Video(super=TypedObject(_id=1, type=VIDEO), videoUrl=youtube.com/java)
Document(super=TypedObject(_id=2, type=DOCUMENT), documentUrl=someurl.com/spring-boot-pdf)
Assessment(super=TypedObject(_id=3, type=ASSESSMENT), marks=78)
Of course you can cast TypedObject to target class you need (I recommend checking instance of before casting) and use:
Video video = (Video) toClazz(readObjects.get(0), objectMapper);
System.out.println(video.getVideoUrl());
I assumed you read whole collection once and you get all types mixed up in one list (as in example in your question). But you can try find documents in MongoDB by field "type" and get data separately for each of type. With this you can easily convert to each type separately.
Project I'm working on has the following come through via MQ:
example.json
{
"templateName": "testTemplate",
"to": [
"support#test.com"
],
"cc": [
"testCc#test.com
],
"bcc": [
"testBcc#test.com
],
"from": "testFrom#test.com",
"subject": "testSubject",
"replacementValues": {
"replacementValue1": "lorem",
"replacementValue2": "ipsum"
},
"jsonObject": {
//omitted for brevity
}
}
And as is, it will map to the following object:
NotificationV1.java
public class NotificationV1 {
private String templateName;
private List<String> to;
private List<String> cc;
private List<String> bcc;
private String from;
private String subject;
private Map<String, String> replacementValues;
private Map<String, String> images;
private Object jsonObject;
//getters & setters omitted for brevity
using the following mapper:
//no special config
notificationMessage = new ObjectMapper().readValue(jsonMessage, EmailNotificationMessage.class);
As part of a project wide refactor, the data class above has been altered to instead look like this:
NotificationV2.java
public class NotificationV2 {
private EmailHeaders emailHeaders;
private TemplateData templateData;
//getters and setters omitted
EmailHeaders.java
public class EmailHeaders {
private String from;
private List<String> toAddresses;
private List<String> ccAddresses;
private List<String> bccAddresses;
private String subject;
//getters and setters omitted
TemplateData.java
public class TemplateData {
private String templateName;
private Map<String, String> replacementValues;
private Map<String, String> images;
private Object jsonObject;
//getters and setters omitted
Naturally, the existing mapping throws errors around unrecognised properties in the json vs. the new object; cant map templateNAme, found emailHeaders and templateData, and so on. I cant change the structure of the json in order to fit the new object but havent found a resource that demonstrates a use case like the above for mapping. Are there annotations I can use on NotificationV2 and/or some sort of mapper configuration I can put together in order to hook all of this up?
To flatten your nested classes, you can use the annotation #JsonUnwrapped.
Example:
public class Parent {
public int age;
public Name name;
}
public class Name {
public String first, last;
}
This would normally be serialized as follows:
{
"age" : 18,
"name" : {
"first" : "Joey",
"last" : "Sixpack"
}
}
By updating the parent to use #JsonUnwrapped, we can flatten the nested objects:
public class Parent {
public int age;
#JsonUnwrapped
public Name name;
}
This will output the following:
{
"age" : 18,
"first" : "Joey",
"last" : "Sixpack"
}
See docs for more information
I have a class that I use as a POJO for responses from a RESTful web service. I'd like to handle all responses with one class, they all come in the same format Status, ErrorCode, ResultCount, Some List of Objects. I'd like to write one class to handle the responses instead of 3. Is there a way when using Json annotations in Jackson to do this? For instance, i have this class:
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonPropertyOrder({ "Status", "ErrorCode", "ResultCount", "Clients" })
public class GetClientsResponse
{
#JsonProperty("Status")
private String status;
#JsonProperty("ErrorCode")
private Integer errorCode;
#JsonProperty("ResultCount")
private Integer resultCount;
#JsonProperty("Clients")
private List<SpiviClient> clients = null;
#JsonIgnore
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
.......
}
I'd like to replace
private List<SpiviClient> clients = null;
with
private List<T> objects = null;
i dont know how to use these annotations for this purpose. Any help is appreciated.
Jackson generics with Variable JsonProperty
I'm having trouble with mapping resulting json data to pojo class with Retrofit. I need to determine Firebase topics by token. This can be eaisly done with Google's json api, as described here: https://developers.google.com/instance-id/reference/server#get_information_about_app_instances
In my case, server response is simlar to this:
{
"applicationVersion": "36",
"connectDate": "2018-02-04",
"attestStatus": "ROOTED",
"application": "<my application id>",
"scope": "*",
"authorizedEntity": "205414012839",
"rel": {
"topics": {
"topic1": {
"addDate": "2018-02-04"
},
"topic2": {
"addDate": "2018-01-31"
}
}
},
"connectionType": "WIFI",
"appSigner": "<hash>",
"platform": "ANDROID"
}
The problem is basically rel and topics structure, because topics is dynamic and field list can by anything and it's unknown. So I can't generate simple POJO to get it mapped by Retrfofit automatically.
Can I force Retrofit to treat topics as single String field, I will able to parse it later to retrieve topics list? Or is there any other soulution?
Any ideas?
If you use gson, you can define rel as a JsonElement. If you use moshi, you can define it as a Map.
for gson:
public class Response{
private String applicationVersion;
private String connectDate;
private String attestStatus;
private String application;
private String scope;
private String authorizedEntity;
private String connectionType;
private String appSigner;
private String platform;
private JsonElement rel;
}
for moshi:
public class Response{
private String applicationVersion;
private String connectDate;
private String attestStatus;
private String application;
private String scope;
private String authorizedEntity;
private String connectionType;
private String appSigner;
private String platform;
private Map<String, Map<String, Map<String, String>>> rel;
}
First of all I've seen this question, but I did not see the full answer to my question and this question was asked 2 years ago.
Introduction:
For example we have an JSON with such structure:
{
"name": "some_name",
"description": "some_description",
"price": 123,
"location": {
"latitude": 456987,
"longitude": 963258
}
}
I can use GSON library for auto parsing this JSON to my object's class.
For this I must create class describing JSON structure, like below:
public class CustomClassDescribingJSON {
private String name;
private String description;
private double price;
private Location location;
// Some getters and setters and other methods, fields, etc
public class Location {
private long latitude;
private long longitude;
}
}
And next I can auto parse JSON to object:
String json; // This object was obtained earlier.
CustomClassDescribingJSON object = new Gson().fromJson(json, CustomClassDescribingJSON.class);
I have a few ways for changing names of fields in my class (for writing more readable code or to follow language guidelines). One of them below:
public class CustomClassDescribingJSON {
#SerializedName("name")
private String mName;
#SerializedName("description")
private String mDescription;
#SerializedName("price")
private double mPrice;
#SerializedName("location")
private Location mLocation;
// Some getters and setters and other methods, fields, etc
public class Location {
#SerializedName("latitude")
private long mLatitude;
#SerializedName("longitude")
private long mLongitude;
}
}
Using same code like above for parsing JSON:
String json; // This object was obtained earlier.
CustomClassDescribingJSON object = new Gson().fromJson(json, CustomClassDescribingJSON.class);
But I could not find a possibility to change the structure of the class. For example, I would like to use next class for parsing the same JSON:
public class CustomClassDescribingJSON {
private String mName;
private String mDescription;
private double mPrice;
private long mLatitude;
private long mLongitude;
}
Questions:
Same as in the header: Is there way to associate arbitrary data structure with GSON parser?
Maybe there are another libraries to do what I want?
Would a custom GSON (de-)serializer help?
See https://sites.google.com/site/gson/gson-user-guide#TOC-Custom-Serialization-and-Deserialization
Simply convert the JSON string into HashMap<String, Object> then populate any type of custom structure by simply iterating it or create a constructor in each custom object class as shown below to populate the fields.
class CustomClassDescribingJSON {
public CustomClassDescribingJSON(Map<String, Object> data) {
// initialize the instance member
}
}
Sample code:
Reader reader = new BufferedReader(new FileReader(new File("resources/json12.txt")));
Type type = new TypeToken<HashMap<String, Object>>() {}.getType();
HashMap<String, Object> data = new Gson().fromJson(reader, type);
System.out.println(new GsonBuilder().setPrettyPrinting().create().toJson(data));
output:
{
"price": 123.0,
"location": {
"latitude": 456987.0,
"longitude": 963258.0
},
"description": "some_description",
"name": "some_name"
}