In our project we parse JSON with Jackson. We set field saved by field channelId. Problem is that channelId field is parsed later than saved. So at the time we want to set field saved field channelId is null. How we can set field dependency in JSON deserialization, so field saved will be set after channelId?
This is part of our JSON data:
"message":{
"data":{
"text":"Some text"
},
"saved_by":[
2715,
1234
],
"some_boolean_field":false,
"channel_id":8162
}
This is our entity class:
#JsonIgnoreProperties(ignoreUnknown = true)
#org.parceler.Parcel(org.parceler.Parcel.Serialization.BEAN)
public class Message {
#JsonProperty("channel_id")
protected long channelId;
protected boolean saved;
#JsonSetter("saved_by")
public void setSavedBy(Set<Long> savedBy) {
saved = savedBy.contains(getUserIdByChannelId(channelId));
}
public long getChannelId() {
return channelId;
}
public void setChannelId(long channelId) {
this.channelId = channelId;
}
public boolean isSaved() {
return saved;
}
public void setSaved(boolean saved) {
this.saved = saved;
}
public void setData(JsonNode data) throws JsonProcessingException {
JsonNode textNode = data.get("text");
text = textNode != null ? textNode.asText() : "";
components = new ArrayList<>();
JsonNode mediaNode = data.get("media");
if (mediaNode != null) {
MessageComponent[] parsedComponents = AppSession.getInstance().getObjectMapper().treeToValue(mediaNode, MessageComponent[].class);
List<MessageComponent> components = Arrays.asList(parsedComponents).subList(0, parsedComponents.length < 4 ? parsedComponents.length : 4);
this.components.addAll(components);
}
mediaCount = components.size();
}
}
Full JSON:
{
"data":{
"serial":66,
"updated_entity":"bookmark",
"bookmark":{
"message":{
"data":{
"text":"hello"
},
"counted_serial":748,
"saved_by":[
26526,
27758
],
"type":"UserMessage",
"is_reviewed":false,
"channel_id":8128,
"id":2841531,
"replied_message_data":null,
"is_blocked":false,
"is_deleted":false,
"updated_at":"2016-11-21T05:59:52.471Z",
"spam_reported_by":[
],
"created_at":"2016-11-19T15:40:17.027Z",
"uuid":"0b6ba58e-f5e1-4ee5-a9da-041dfc2c85cd",
"liked_by":[
],
"user":{
"last_name":"M",
"id":4537,
"first_name":"John",
"is_deleted":false,
"avatar_thumb":"https:\/\/cdn.site.org\/uploads\/99ef4d68-6eaf-4ba6-aafa-74d1cf895d71\/thumb.jpg"
},
"serial":934
},
"id":6931,
"created_at":"2016-11-21T05:59:52.459Z",
"is_deleted":false,
"updated_at":"2016-11-21T05:59:52.459Z"
}
},
"type":"action_performed"
}
It's a bit hackish, but by making the Message class its own deserialization-builder, you get a kind of "ready for bean creation"-event in which you have access to all of the properties.
My suggestion is that you try the following :
#JsonDeserialize(builder = Message.class)
public class Message {
...
#JsonSetter("saved_by")
public void setSavedBy(Set<Long> savedBy) {
// Merely store the value for later use.
this.savedBy = savedBy;
}
...
public Message build() {
// Calculate value of "saved" field.
this.saved = this.savedBy.contains(getUserIdByChannelId(this.channelId));
return this;
}
// Handling the added challenge.
#JsonProperty("data")
public void setData(JsonNode data) throws JsonProcessingException {
...
}
}
The above takes advantage of the default settings of the JsonPOJOBuilder annotation, namely that the default value for buildMethodName is build.
Related
There is a class defined follows:
#Data // lombok
public class MyData {
#Required // my custom annotation
String testValue1;
Integer testValue2;
}
And myData is instantiated like that:
MyData myData = new MyData();
myData.setTestValue1("test1");
myData.setTestValue2(123);
I want to serialize myData as json string as follows:
{
"testValue1": {
"type": "String",
"isRequired": "true",
"value": "test1"
},
"testValue2": {
"type": "Integer",
"isRequired": "false",
"value": "123"
},
}
Is there a good way to create json string?
edit|
I put quotes on json string that to be able to valid.
I want to set key as field name and create additional field information.
set field type on "type" key and
if field has #Required annotation, set true on "isRequired" and
set instantiated field value on "value".
So I played a bit around with Jackson Serialization and came to this result (certainly unfinished and not fully tested, but works with your given object).:
Module to make Spring / Jackson known of the new Serializer.
#JsonComponent
public class TestSerializerModule extends SimpleModule {
#Override
public String getModuleName() {
return TestSerializerModule.class.getSimpleName();
}
#Override
public Version version() {
return new Version(
1,
0,
0,
"",
TestSerializerModule.class.getPackage().getName(),
"TestModule"
);
}
#Override
public void setupModule(SetupContext context) {
context.addBeanSerializerModifier(new BeanSerializerModifier() {
#Override
public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
if (beanDesc.getBeanClass().equals(MyData.class)) { //Add some smart logic here to identify your objects
return new TestSerializer();
}
return serializer;
}
});
}
}
Then the Serialisier itself:
public class TestSerializer extends StdSerializer<Object> {
protected TestSerializer() {
super(Object.class);
}
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
ClassIntrospector classIntrospector = provider.getConfig().getClassIntrospector();
BasicBeanDescription beanDescription = (BasicBeanDescription) classIntrospector.forSerialization(provider.getConfig(), provider.constructType(value.getClass()), null);
// Start of the MyValue Object
gen.writeStartObject();
beanDescription.findProperties().forEach(p -> {
// Requiered if Annoation is present
boolean required = p.getField().hasAnnotation(Required.class);
try {
// Write all the wanted fields
gen.writeFieldName(p.getName());
gen.writeStartObject();
gen.writeBooleanField("isRequired", required);
gen.writeStringField("type", p.getField().getRawType().getSimpleName());
gen.writeFieldName("value");
Object value1 = p.getGetter().getValue(value);
// Use existing serializer for the value provider.findValueSerializer(value1.getClass()).serialize(value1, gen, provider);
gen.writeEndObject();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
);
gen.writeEndObject();
}
}
Running this test :
#JsonTest
class TestSerializerTest {
#Autowired
ObjectMapper objectMapper;
#Test
public void testSerializer() throws Exception {
MyData value = new MyData();
value.setTestValue1("test1");
value.setTestValue2(123);
String s = objectMapper.writeValueAsString(value);
System.out.println(s);
}
}
gives me this output:
{"testValue1":{"isRequired":false,"type":"String","value":"test1"},"testValue2":{"isRequired":false,"type":"Integer","value":123}}
Hope that gives you an idea where to start and how to proceed from here!
gson null when not deserialize
public class Mode {
#Expose(deserialize = false)
public final List<String> list;
public Mode(List<String> list) {
this.list = list;
}
public List<String> getList() {
return list;
}
}
list only serialize not deserialize
public class Entity {
public Mode setting = new Mode(Arrays.asList("1", "2"));
}
add deserialization exclusion strategy:
Gson gson = new GsonBuilder().setPrettyPrinting().addDeserializationExclusionStrategy(new ExclusionStrategy() {
#Override
public boolean shouldSkipField(FieldAttributes f) {
Expose annotation = f.getAnnotation(Expose.class);
if (annotation == null) {
return false;
}
return !annotation.deserialize();
}
#Override
public boolean shouldSkipClass(Class<?> clazz) {
return false;
}
}).create();
var s = """
{
"setting": {
"list": [
"1",
"2",
"3"
]
}
}
""";
System.out.println(gson.fromJson(s, Entity.class).setting.getList());
The list field of the Mode class is null when deserialize the Entity class
Perfect solution
//Step.1 get json object
JsonObject jsonObject = gson().fromJson("", JsonObject.class);
//Step.2 get all field of the entity
for (Field configField : klass.getDeclaredFields()) {
configField.setAccessible(true);
if (!jsonObject.has(configField.getName())) {
continue;
}
//Step.3 deserialize
Object setting = gson().fromJson(jsonObject.get(configField.getName()).toString(), configField.getType());
//Step.3.1 get all field of the setting
for (Field settingField : setting.getClass().getDeclaredFields()) {
settingField.setAccessible(true);
if (settingField.isAnnotationPresent(Expose.class)) {
if (!settingField.getAnnotation(Expose.class).deserialize()) {
Field declaredField = ReflectUtil.getField(setting.getClass(), settingField.getName());
//Step.3.2 use the value from entity to set the value of setting
declaredField.set(setting, ReflectUtil.getFieldValue(ReflectUtil.getFieldValue(entity, configField.getName()), settingField.getName()));
}
}
}
//Step.4 use the value from step 3 to set the value of entity
configField.set(entity, setting);
}
I have a json format which I am converting into Java Object Model using Jackson API. I am using Jaxsonxml 2.1.5 parser. The json response is as shown below.
{
"response": {
"name": "states",
"total-records": "1",
"content": {
"data": {
"name": "OK",
"details": {
"id": "1234",
"name": "Oklahoma"
}
}
}
}
}
Now json response format has changed. If the total-records is 1 the details will be an object with id and name attributes. But if the total-records is more than 1 then the details will be an array of object like below:
{
"response": {
"name": "states",
"total-records": "4",
"content": {
"data": {
"name": "OK",
"details": [
{
"id": "1234",
"name": "Oklahoma"
},
{
"id": "1235",
"name": "Utah"
},
{
"id": "1236",
"name": "Texas"
},
{
"id": "1237",
"name": "Arizona"
}
]
}
}
}
}
My Java Mapper class looks like below with earlier json response.
#JsonIgnoreProperties(ignoreUnknown = true)
public class MapModelResponseList {
#JsonProperty("name")
private String name;
#JsonProperty("total-records")
private String records;
#JsonProperty(content")
private Model model;
public Model getModelResponse() {
return model;
}
public void setModel(Model model) {
this.model = model;
}
}
Client Code
package com.test.deserializer;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com..schema.model.Person;
public class TestClient {
public static void main(String[] args) {
String response1="{\"id\":1234,\"name\":\"Pradeep\"}";
TestClient client = new TestClient();
try {
Person response = client.readJSONResponse(response1, Person.class);
} catch (Exception e) {
e.printStackTrace();
}
}
public <T extends Object> T readJSONResponse(String response, Class<T> type) {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
T result = null;
try {
result = mapper.readValue(response, type);
} catch (Exception e) {
e.printStackTrace();
}
return (T) result;
}
}
Now based on the total-records how to handle to mapping to either a Model or list of Model Object. Please let me know.
You need a custom deserializer. The idea is to mix and match object processing with tree processing. Parse objects where possible but use the tree (JSONNode) for custom handling.
On the MapModelResponseList, remove the records property and add a List<Data> array where Data is just a holder class for the id/name pairs. You can get the total records by returning the size of this list.
In the deserializer, do the following:
public final class MapModelDeserializer extends BeanDeserializer {
public MapModelDeserializer(BeanDeserializerBase src) {
super(src);
}
protected void handleUnknownProperty(JsonParser jp, DeserializationContext ctxt, Object beanOrClass, String propName) throws IOException, JsonProcessingException {
if ("content".equals(propName)) {
MapModelResponseList response = (MapModelResponseList) beanOrClass;
// this probably needs null checks!
JsonNode details = (JsonNode) jp.getCodec().readTree(jp).get("data").get("details");
// read as array and create a Data object for each element
if (details.isArray()) {
List<Data> data = new java.util.ArrayList<Data>(details.size());
for (int i = 0; i < details.size(); i++) {
Data d = jp.getCodec().treeToValue(details.get(i), Data.class);
data.add(d);
}
response.setData(data);
}
// read a single object
else {
Data d = jp.getCodec().treeToValue(details, Data.class);
response.setData(java.util.Collections.singletonList(d));
}
super.handleUnknownProperty(jp, ctxt, beanOrClass, propName);
}
Note that you do not implement deserialize() - the default implementation is used to create the MapModelResponseList as normal. handleUknownProperty() is used to deal with the content element. Other data you don't care about is ignored due to #JsonIgnoreProperties(ignoreUnknown = true) in the super call.
This is a late answer, but I solve it in a different way. It can work by catching it in Object like this:
#JsonProperty("details")
public void setDetails(Object details) {
if (details instanceof List) {
setDetails((List) details);
} else if (details instanceof Map) {
setDetails((Map) details);
}
}
public void setDetails(List details) {
// your list handler here
}
public void setDetails(Map details) {
// your map handler here
}
I'm trying to deserialize a JSON which containes a String and a list of objects in my Spring web application.
JSON
[
{
"jsonrpc":"2.0",
"result":[
{
"event":{
"id":"27809810",
"name":"Spezia v Trapani",
"countryCode":"IT",
"timezone":"Europe/London",
"openDate":"2016-05-28T16:30:00.000Z"
},
"marketCount":13
},
{
"event":{
"id":"27811083",
"name":"Torino U19 v Atalanta U19",
"countryCode":"IT",
"timezone":"Europe/London",
"openDate":"2016-05-29T16:15:00.000Z"
},
"marketCount":18
},
...
]
My classes are:
ListEventsResponse class
public class ListEventsResponse {
private String jsonrpc;
private List<ListEventsResult> result;
public ListEventsResponse() { }
public String getJsonrpc() {
return jsonrpc;
}
public void setJsonrpc(String jsonrpc) {
this.jsonrpc = jsonrpc;
}
public List<ListEventsResult> getResult() {
return result;
}
public void setResult(List<ListEventsResult> result) {
this.result = result;
}
}
ListEventsResult class
public class ListEventsResult {
private Event event;
private int marketCount;
public ListEventsResult() { }
public Event getEvent() {
return event;
}
public void setEvent(Event event) {
this.event = event;
}
public int getMarketCount() {
return marketCount;
}
public void setMarketCount(int marketCount) {
this.marketCount = marketCount;
}
}
I have also Event class, composed by 5 String (id, name, etc.).
Controller
[...]
ListEventsResponse listEvents = new Gson().fromJson(response.toString(), ListEventsResponse.class);
List<ListEventsResult> eventsList = listEvents.getResult();
return new ModelAndView("page", "eventsList", eventsList);
My .jsp page
[...]
<c:forEach items="${eventsList}" var="listEventsResult">
Match: <c:out value="${listEventsResult.name}"/>
</c:forEach>
[...]
My code runs and doesn't give any error, but no match is shown on my page, in fact listEvents doesn't contains any object.
I can't understand how to deserialize properly the list of objects, so my question is: which logic is behind the deserialization of a json which contains a list of objects?
I post my code just to explain better my problem.
As you have a Json Array as response , you need to deserialize like below
Gson gson = new Gson();
Type type = new TypeToken<List<ListEventsResponse>>(){}.getType();
List<ListEventsResponse> events = (List<ListEventsResponse>) gson.fromJson(response.toString(), type);
My Json looks something like (and its unmodifiable)
{
....
"Sale": [
"SaleLines": {
"SaleLine": [
{
"Item": {
"Prices": {
"ItemPrice": [
{
"amount": "100",
"useType": "Default"
},
{
"amount": "100",
"useType": "MSRP"
}
]
},
}
......
......
}
]
"calcDiscount": "0",
"calcSubtotal": "500",
}
]
}
The java POJO code looks like
public static class SaleLines {
#JsonProperty("SaleLine")
private SaleLineObject[] saleLineObject;
public SaleLineObject[] getSaleLineObject() { return saleLineObject; }
public void setSaleLineObject(SaleLineObject[] saleLineObject) { this.saleLineObject = saleLineObject; }
}
public static class SaleLineObject {
private SaleLine saleLine;
public SaleLine getSaleLine() {
return saleLine;
}
public void setSaleLine(SaleLine saleLine) {
this.saleLine = saleLine;
}
}
public static class SaleLine {
#JsonProperty("itemID")
private String itemId; //line_item_nk
#JsonProperty("unitPrice")
private String unitPrice;
....
}
#JsonPropertyOrder({"total", "calcSubTotal", "calcDiscount"})
public static class Sale {
private String saleTotal, calcSubtotal, calcDiscount;
private int salesValueWOVat;
#JsonProperty("SaleLines")
SaleLines saleLine;
#JsonCreator
public Sale (#JsonProperty("total")String saleTotal,
#JsonProperty("calcSubtotal")String calcSubtotal,
#JsonProperty("calcDiscount")String calcDiscount,
#JsonProperty("SaleLines")SaleLines saleLine,
) {
this.saleTotal = saleTotal;
this.calcSubtotal = calcSubtotal;
this.calcDiscount = calcDiscount;
this.saleLine = saleLine;
setSalesValueWOVat();
}
// getter and setters
}
#SuppressWarnings({ "rawtypes" })
public static <E, T extends Collection> T readFromJsonAndFillType (
String json,
Modules module,
Class <T> collectionType,
Class <E> elementType)
throws JsonParseException, JsonMappingException, IOException {
ObjectMapper objMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
TypeFactory tf = objMapper.getTypeFactory();
JsonNode node = objMapper.readTree(json).get(module.jsonFieldName);
return objMapper.readValue(node.toString(),
tf.constructCollectionType(collectionType, elementType));
}
In main
ArrayList<Sale> saleList = readFromJsonAndFillType(
saleJSON,
Modules.SALE,
ArrayList.class,
Sale.class);
for (Sale sale: saleList) {
System.out.println(sale.toString());
}
I know this question has been asked multiple times and even I took help from for eg
Can not deserialize instance of java.util.ArrayList out of START_OBJECT token
But still I cannot get through this error
I know this question has been asked multiple times & everyone getting resolved there problems with different ways. Whenever you find "Can not deserialized instance of out of START_OBJECT token". it's generally occur when you trying to get object which is not actually same in json format (means json starting object is different not as you guys are converting).
For Ex:- Json returning first object is Boolean but unfortunately you are converting is to List<Object> then you will having this error.
I would suggest to have a look to read format using below code than convert it as per the object returning.
ObjectMapper objectMapper = new ObjectMapper();
Map<?,?> empMap = objectMapper.readValue(new FileInputStream("employee.json"),Map.class);
for (Map.Entry<?,?> entry : empMap.entrySet())
{
System.out.println("\n----------------------------\n"+entry.getKey() + "=" + entry.getValue()+"\n");
}
Get the key & convert the value as per the object returning.
For reference:- https://dzone.com/articles/processing-json-with-jackson