Does Jackson not pretty print values annotated by #JsonRawValue? - java

I store JSON in my database and want to include this JSON in an API response as-is, without de-serializing before serializing the data.
The data itself resides in a wrapper object. When serializing this wrapper, it appears the JSON from my database isn't pretty-printed alongside the rest of the data, giving really weird-looking responses.
I have written some example code to outline my issue:
import com.fasterxml.jackson.annotation.JsonRawValue;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class JacksonTest {
private static final String EXPECTED_OUTPUT = "{\n" +
" \"wrapper\" : {\n" +
" \"data\" : {\n" +
" \"raw\" : \"test\"\n" +
" }\n" +
" }\n" +
"}";
private static final String RAW_JSON = "{\n" +
" \"raw\" : \"test\"\n" +
"}";
static class Pojo {
#JsonRawValue
private final String data;
public Pojo(String data) {
this.data = data;
}
public String getData() {
return data;
}
}
static class Wrapper {
private final Pojo wrapper;
public Wrapper() {
wrapper = new Pojo(RAW_JSON);
}
#SuppressWarnings("unused")
public Pojo getWrapper() {
return wrapper;
}
}
#Test
void shouldEqual() throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
String output = mapper.writeValueAsString(new Wrapper());
assertThat(output).isEqualTo(EXPECTED_OUTPUT);
}
}
This test fails with the following output:
{
"wrapper" : {
"data" : {
"raw" : "test"
}
}
}
While I expect jackson to give me the following output:
{
"wrapper" : {
"data" : {
"raw" : "test"
}
}
}
Is there any way to "fix" the indenting of the raw data that's annotated with #JsonRawValue?

Maybe with the following code your test will pass :
Object json = mapper.readValue(input, Object.class);
String indented = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(json);
You can check that stackoverflow question and its own answers from which the code I have written upper was coming from, from the accepted answer :
Convert JSON String to Pretty Print JSON output using Jackson

Related

Unable to deserialise via mixin

Attached is my json. I am receiving this from an endpoint. I have the object type in my consuming app. the object contains; success, message and loggedInMember (which is an object). In the json, the feeClasses contains a lot of fields and objects etc which is why i have not included the whole json. I am only interested in the success and message fields.
{
"header":{
"messageId":null,
"receivedTimestamp":1611246394839,
"replyTo":null,
"redelivered":false,
"originator":null
},
"internalId":null,
"auditPoints":[
],
"success":true,
"message":"",
"loggedInMember":{
"feeClasses":{
...CONTAINS A LOT OF FIELDS...
}
}
}
I am trying to map this response to the object type, hence essentially leaving loggedInMembers null. This is my test I am trying to run:
public void test() throws JsonProcessingException
{
//String json = "{\"header\":{\"messageId\":null,\"bucketNumber\":null,\"senderSubId\":null,\"senderLocationId\":null,\"onBehalfOfCompId\":null,\"onBehalfOfSubId\":null,\"onBehalfOfLocationId\":null,\"correlationId\":null,\"receivedTimestamp\":1611246394839,\"replyTo\":null,\"redelivered\":false,\"originator\":null},\"internalId\":null,\"auditPoints\":[],\"success\":true,\"message\":\"\",\"loggedInMember\":{\"memberLoginName\":\"BMARTINTEST\",\"memberId\":\"201901241246290000036402D\",\"settlementAccountIds\":[\"201901241246290000036491D\"],\"parentMemberId\":\"1\",\"firmId\":\"990\",\"memberType\":\"INDIVIDUAL\",\"memberAccountType\":\"PROD\",\"password\":\"D1208B304FD7AA6187690A389A5040C1D9B07643\",\"feeClasses\":{\"byId\":{\"201902120947520000559606D\":{\"memberLoginName\":\"BMARTINTEST\",\"feeClassId\":\"201508041827550000942152D\",\"memberFeeClassId\":\"201902120947520000559606D\",\"allocatedDate\":{\"year\":2019,\"month\":2,\"day\":12,\"timeMillis\":1549929600000},\"firstUsedForTradeDate\":{\"year\":2019,\"month\":2,\"day\":12,\"timeMillis\":1549929600000},\"firstUsedForSettlementDate\":null,\"usableFromDate\":{\"year\":2019,\"month\":2,\"day\":12,\"timeMillis\":1549929600000},\"usableToDate\":{\"year\":2019,\"month\":2,\"day\":19,\"timeMillis\":1550534400000},\"usableToTimestamp\":1550613600000,\"usableBusinessDaysAllocated\":6,\"usableBusinessDaysRemaining\":0,\"narrative\":\"Bonus assigned to member at first-time funding of amount 4000.00 : Set expiration date/time\",\"disabled\":false,\"usableForTrade\":true,\"usableForSettlement\":true},\"202001290940390000868824D\":{\"memberLoginName\":\"BMARTINTEST\",\"feeClassId\":\"202001290940340000776406D\",\"memberFeeClassId\":\"202001290940390000868824D\",\"allocatedDate\":{\"year\":2020,\"month\":1,\"day\":29,\"timeMillis\":1580256000000},\"firstUsedForTradeDate\":null,\"firstUsedForSettlementDate\":null,\"usableFromDate\":{\"year\":2020,\"month\":1,\"day\":6,\"timeMillis\":1578268800000},\"usableToDate\":{\"year\":2020,\"month\":2,\"day\":27,\"timeMillis\":1582761600000},\"usableToTimestamp\":1582840800000,\"usableBusinessDaysAllocated\":0,\"usableBusinessDaysRemaining\":0,\"narrative\":\"Added NO_FEES_CLASS\",\"disabled\":false,\"usableForTrade\":true,\"usableForSettlement\":true},\"201901241246290000036417D\":{\"memberLoginName\":\"BMARTINTEST\",\"feeClassId\":\"201508041736360000943781D\",\"memberFeeClassId\":\"201901241246290000036417D\",\"allocatedDate\":{\"year\":2019,\"month\":1,\"day\":24,\"timeMillis\":1548288000000},\"firstUsedForTradeDate\":null,\"firstUsedForSettlementDate\":null,\"usableFromDate\":{\"year\":2019,\"month\":1,\"day\":24,\"timeMillis\":1548288000000},\"usableToDate\":null,\"usableToTimestamp\":null,\"usableBusinessDaysAllocated\":0,\"usableBusinessDaysRemaining\":0,\"narrative\":null,\"disabled\":false,\"usableForTrade\":true,\"usableForSettlement\":true}},\"empty\":false},\"legalName\":\"Martin Birch\",\"taxId\":\"345335454\",\"taxCountryId\":\"US\",\"currency\":\"USD\",\"lastTradeId\":null,\"introducingBrokerMemberId\":null,\"introducingBrokerMemberName\":null,\"introducingBrokerMemberCode\":null,\"clearedByMemberId\":\"SECOND_TEST\",\"clearedByMemberLoginName\":null,\"memberProblems\":[],\"emailNotificationEnabled\":true,\"rtafLevelId\":0,\"rtafAmount\":0,\"maxNumberOfPositionAccounts\":1,\"ciciIdentifier\":null,\"traderRequired\":false,\"interestClass\":\"INDIVIDUAL\",\"memberCreatedDate\":1548333989000,\"parentMemberLoginNames\":[\"NADEX.COM\",\"NADEX\"],\"demoStartDate\":null,\"demoEndDate\":null,\"clientIdMaxLimit\":null,\"memberAccountApplicationFieldData\":null,\"rank\":0,\"uuid\":\"201901241246290000036395D\",\"referrerId\":\"raf4qam5h00s36d\",\"testMember\":false},\"allReplyToSource\":[],\"sendToOriginatorOnly\":false}";
String json = "{\n" +
" \"header\":{\n" +
" \"messageId\":null,\n" +
" \"receivedTimestamp\":1611246394839,\n" +
" \"replyTo\":null,\n" +
" \"redelivered\":false,\n" +
" \"originator\":null\n" +
" },\n" +
" \"internalId\":null,\n" +
" \"auditPoints\":[\n" +
" \n" +
" ],\n" +
" \"success\":true,\n" +
" \"message\":\"\",\n" +
" \"loggedInMember\":{\n" +
" \"feeClasses\":{\n" +
" \n" +
" }\n" +
" }\n" +
"}";
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
objectMapper.addMixIn(LogonResponseMessage.class, LogonResponseMixin.class);
LogonResponseMessage responseMessage = objectMapper.readValue(json, LogonResponseMessage.class);
System.out.println(responseMessage);
}
My mixin:
public abstract class LogonResponseMixin
{
LogonResponseMixin(#JsonProperty("success") boolean success, #JsonProperty("message") String message){};
#JsonIgnore
abstract Member loggedInMember();
#JsonIgnore
abstract MemberFeeClasses feeClasses();
#JsonIgnore
abstract Header header();
}
I am getting the following error: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of domain.xyz.MemberFeeClasses (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
Am i creating the mixin wrong? I have asked in a previous question and using mixin was the general consensus but it doesn't seem to play ball with me.
Thank you.
The reason why MemberFeeClasses cannot be constructed is the same as your initial problem, just add a mixin for all classes
See the example below:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import lombok.Setter;
import java.io.IOException;
public class Test {
//------------------------------------------------//
// PART 1 - MAIN TEST METHOD
//------------------------------------------------//
public static void main(String[] args) throws IOException {
String json = "{\n" +
" \"header\":{" +
" },\n" +
" \"success\":true,\n" +
" \"message\":\"\",\n" +
" \"loggedInMember\":{\n" +
" \"feeClasses\":{\n" +
" \"amount\": \"20\"\n" +
" }\n" +
" }\n" +
"}";
ObjectMapper objectMapper = new ObjectMapper();
//objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
objectMapper.addMixIn(LogonResponseMessage.class, LogonResponseMixin.class);
objectMapper.addMixIn(Member.class, MemberMixin.class);
objectMapper.addMixIn(MemberFee.class, MemberFeeMixin.class);
LogonResponseMessage responseMessage = objectMapper.readValue(json, LogonResponseMessage.class);
System.out.println(responseMessage.loggedInMember.feeClasses.amount);
}
//------------------------------------------------//
// PART 2 - CREATOR MIXIN
//------------------------------------------------//
static abstract class LogonResponseMixin {
LogonResponseMixin(#JsonProperty("success") boolean success,
#JsonProperty("message") String message,
#JsonProperty("header") Header header,
#JsonProperty("loggedInMember") Member member) {
}
}
static abstract class MemberMixin {
MemberMixin(#JsonProperty("feeClasses") MemberFee feeClasses) {
}
}
static abstract class MemberFeeMixin {
#JsonCreator
MemberFeeMixin(#JsonProperty("amount") String amount) {
}
}
//------------------------------------------------//
// PART 3 - EXAMPLE CLASS DEFINITION
//------------------------------------------------//
static class Header {
}
#Getter
#Setter
static class Member {
private MemberFee feeClasses;
public Member(MemberFee feeClasses) {
this.feeClasses = feeClasses;
}
}
#Getter
#Setter
static class MemberFee {
private String amount;
public MemberFee(String amount) {
this.amount = amount;
}
}
#Getter
#Setter
static class LogonResponseMessage {
private boolean success;
private String message;
private Header header;
private Member loggedInMember;
public LogonResponseMessage(boolean success, String message, Header header, Member member) {
this.success = success;
this.message = message;
this.header = header;
this.loggedInMember = member;
}
}
}

Java, Jackson, Custom deserializer with extended classes

Using Jackson 2.10., I am trying to write a custom deserializer for a base class, but I have to deserialize fields with unknown field name. Then there are extended class that can also extend this serializer.
I have tried to use the #AnyGetter, and #AnySetter to accomplish it, and it kind of does work. Now I am just wondering if there is a way to do it through a custom deserializer.
I could do it with a base class, but it fails when some class extends it.
Here is the sample of what I have done.
The following is just the base class and its serializer and how I used in in the main.
//BaseClass
#JsonDeserialize(using = BaseClassDeserializer.class)
public static class BaseClass {
private ObjectNode customFields = JsonNodeFactory.instance.objectNode();
private int baseInt;
public int getBaseInt() {
return baseInt;
}
public void setBaseInt(int baseInt) {
this.baseInt = baseInt;
}
public JsonNode getCustomFields() {
return customFields;
}
public void setCustomFields(ObjectNode customFields) {
this.customFields = customFields;
}
public void putCustomFields(String key, JsonNode node) {
this.customFields.set(key, node);
}
}
// BaseClassDeserializer
public static class BaseClassDeserializer extends StdDeserializer<BaseClass> {
public BaseClassDeserializer() {
this(null);
}
public BaseClassDeserializer(Class<?> vc) {
super(vc);
}
#Override
public BaseClass deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JsonProcessingException {
BaseClass result = new BaseClass();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
result.setBaseInt((Integer) ((IntNode) node.get("baseInt")).numberValue());
node.fieldNames();
Iterator<String> iterator = node.fieldNames();
while (iterator.hasNext()) {
String fieldName = iterator.next();
if (!"baseInt".equals(fieldName)) {
result.putCustomFields(fieldName, node.get(fieldName));
}
}
return result;
}
}
// main
public static void main(String[] args) throws JsonProcessingException {
String json = "{\n"
+ "\t\"baseInt\": 1,\n"
+ "\t\"customObject\" : {\n"
+ "\t\t\"key\": \"value\"\n"
+ "\t},\n"
+ "\t\"customString\" : \"STRING\",\n"
+ "\t\"extendedString\" : \"STRING\"\n"
+ "}";
ObjectMapper mapper = new ObjectMapper();
BaseClass myClass = mapper.readValue(json, BaseClass.class);
}
By going looking through the debugger, the fields are successfully loaded.
Now I am trying to extend BaseClass
// ExtendedClass
public static class ExtendedClass extends BaseClass {
#JsonProperty("extendedString")
private String extendedString;
public String getExtendedString() {
return extendedString;
}
public void setExtendedString(String extendedString) {
this.extendedString = extendedString;
}
}
public static void main(String[] args) throws JsonProcessingException {
String json = "{\n"
+ "\t\"baseInt\": 1,\n"
+ "\t\"customObject\" : {\n"
+ "\t\t\"key\": \"value\"\n"
+ "\t},\n"
+ "\t\"customString\" : \"STRING\",\n"
+ "\t\"extendedString\" : \"STRING\"\n"
+ "}";
ObjectMapper mapper = new ObjectMapper();
ExtendedClass myClass = mapper.readValue(json, ExtendedClass.class);
}
And this crashes with a
BaseClass cannot be cast to ExtendedClass exception.
I am guessing I have to pass along the deserialization to the child class' deserializer, but I cannot figure out how.
In your deserialiser you always return object of type BaseClass and it can not be cast to ExtendedClass. You need to implement type recognition feature in your deserialiser. In your case, returned type depends from properties JSON payload contains. If JSON payload contains extendedString property you know you need to return ExtendedClass in other case just return BaseClass. Your deserialiser could look like below:
class BaseClassDeserializer extends StdDeserializer<BaseClass> {
public BaseClassDeserializer() {
super(BaseClass.class);
}
#Override
public BaseClass deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
ObjectNode root = jsonParser.readValueAsTree();
List<String> names = getNames(root);
BaseClass result = findAndInitCustomType(names, root);
result = initBase(names, result, root);
initCustomFields(names, root, result);
return result;
}
private void initCustomFields(List<String> names, ObjectNode root, BaseClass result) {
for (String name : names) {
result.putCustomFields(name, root.get(name));
}
}
private BaseClass findAndInitCustomType(List<String> names, ObjectNode root) {
final String extendedString = "extendedString";
if (names.contains(extendedString)) {
ExtendedClass result = new ExtendedClass();
result.setExtendedString(root.get(extendedString).asText());
names.remove(extendedString);
return result;
}
// else - check other custom fields for another types.
// if not available return null
return null;
}
private BaseClass initBase(List<String> names, BaseClass baseClass, ObjectNode root) {
if (baseClass == null) {
baseClass = new BaseClass();
}
final String baseInt = "baseInt";
if (names.contains(baseInt)) {
baseClass.setBaseInt(root.get(baseInt).asInt());
names.remove(baseInt);
}
return baseClass;
}
private List<String> getNames(ObjectNode root) {
List<String> names = new ArrayList<>();
root.fieldNames().forEachRemaining(names::add);
return names;
}
}
Example usage:
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class JsonApp {
public static void main(String[] args) throws Exception {
String baseJson = "{"
+ "\"baseInt\": 1,\n"
+ "\t\"customObject\" : {\n"
+ "\t\t\"key\": \"value\"\n"
+ "\t},\n"
+ "\t\"customString\" : \"STRING\""
+ "}";
String extendedJson = "{"
+ "\t\"baseInt\": 1,\n"
+ "\t\"customObject\" : {\n"
+ "\t\t\"key\": \"value\"\n"
+ "\t},\n"
+ "\t\"customString\" : \"STRING\",\n"
+ "\t\"extendedString\" : \"STRING\"\n"
+ "}";
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.readValue(baseJson, BaseClass.class));
System.out.println(mapper.readValue(extendedJson, BaseClass.class));
System.out.println(mapper.readValue(extendedJson, ExtendedClass.class));
}
}
Above code prints:
BaseClass{customFields={"customObject":{"key":"value"},"customString":"STRING"}, baseInt=1}
ExtendedClass{extendedString='STRING'} BaseClass{customFields={"customObject":{"key":"value"},"customString":"STRING"}, baseInt=1}
ExtendedClass{extendedString='STRING'} BaseClass{customFields={"customObject":{"key":"value"},"customString":"STRING"}, baseInt=1}
Improvements:
In BaseClass class instead of ObjectNode use Map<String, JsonNode> or even Map<String, Object>. It is not a good idea to tie POJO classes with 3-rd party libraries.
You do not need to use #JsonProperty annotation if you manually handle deserialisation.

How do I parse Json data with Gson when not everything is labelled

I have happily been using Google Gson to parse extract some JSON metadata of the form
{
"lowlevel": {
"average_loudness": 0.570070445538
},
"rhythm": {
"beats_count": 502,
"bpm": 128.347702026
},
"tonal": {
"chords_changes_rate": 0.0534749031067
"tuning_diatonic_strength": 0.431238204241,
"tuning_equal_tempered_deviation": 0.164615109563,
"tuning_frequency": 434.193115234,
"tuning_nontempered_energy_ratio": 0.847496032715
}
}
Using this
public class AcousticBrainzLowlevelWrapper
{
private AcousticBrainzLowLevelRhythm rhythm;
private AcousticBrainzLowLevelTonal tonal;
public AcousticBrainzLowLevelRhythm getRhythm()
{
return rhythm;
}
public void setRhythm(AcousticBrainzLowLevelRhythm rhythm)
{
this.rhythm = rhythm;
}
public AcousticBrainzLowLevelTonal getTonal()
{
return tonal;
}
public void setTonal(AcousticBrainzLowLevelTonal tonal)
{
this.tonal = tonal;
}
}
and
AcousticBrainzLowlevelWrapper low = gson.fromJson(result, AcousticBrainzLowlevelWrapper.class) ;
(Full JSON can be seen here)
but now the API has been extended to allow multiple lookups such as this url
which now returns
{
"96685213-a25c-4678-9a13-abd9ec81cf35": {
"0": {
"lowlevel": {
"average_loudness": 0.570070445538
},
"rhythm": {
"beats_count": 502,
"bpm": 128.347702026
},
"tonal": {
"chords_changes_rate": 0.0534749031067
"tuning_diatonic_strength": 0.431238204241,
"tuning_equal_tempered_deviation": 0.164615109563,
"tuning_frequency": 434.193115234,
"tuning_nontempered_energy_ratio": 0.847496032715
}
}
.....
"78787888-a25c-4678-9a13-abd9ec81cf35": {
"0": {
"lowlevel": {
......
..
The difference being that the json doesn't define what "96685213-a25c-4678-9a13-abd9ec81cf35" and "78787888-a25c-4678-9a13-abd9ec81cf35" are, or what "0" is.
So I know what they represent (MusicBrainzRecording and offset) but I cannot create a class like AcousticBrainzLowlevelWrapper to represent this, so how do I parse this new api.
Update
I tried creating
public class AcousticBrainzLowLevelList
{
private Map<String, AcousticBrainzLowlevelWrapper> data = new HashMap<>();
public Map<String, AcousticBrainzLowlevelWrapper> getData()
{
return data;
}
public void setData(Map<String, AcousticBrainzLowlevelWrapper> data)
{
this.data = data;
}
}
and then calling
AcousticBrainzLowLevelList lowMap = gson.fromJson(result, AcousticBrainzLowLevelList.class) ;
but nothing get added to the map. Unsuprisingly because data I dont' see how can i give a name since there is no consistent name at the top level.
It seems to me that your input JSON could be parsed to produce a Java class of type Map<String,Map<Integer,AcousticBrainzLowlevelWrapper>> :
Type type = new TypeToken<Map<String,Map<Integer,AcousticBrainzLowlevelWrapper>>>(){}.getType();
Map<String,Map<Integer,AcousticBrainzLowlevelWrapper>> result = gson.fromJson(json, type);
As I wrote it, I might as well post it:
Similar to Maurice's answer
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.Map;
public class Main {
private final static String jsonSingle =
"{ \"attribute1\": \"value1\", \"attribute2\": \"value2\" }";
private final static String jsonMultiple =
"{\n" +
" \"96685213-a25c-4678-9a13-abd9ec81cf35\": {\n" +
" \"0\": { \"attribute1\": \"value1\", \"attribute2\": \"value2\" }\n" +
" },\n" +
" \"78787888-a25c-4678-9a13-abd9ec81cf35\": {\n" +
" \"0\": { \"attribute1\": \"value3\", \"attribute2\": \"value4\" }\n" +
"}}";
public static void main(String[] args) {
MyBean bean = new Gson().fromJson(jsonSingle, MyBean.class);
System.out.println(bean);
Type type = new TypeToken<Map<String, Map<String, MyBean>>>(){}.getType();
Map<String, String> myMap = new Gson().fromJson(jsonMultiple, type);
System.out.println(myMap);
}
}
MyBean class:
class MyBean {
String attribute1;
String attribute2;
public String getAttribute1() {
return attribute1;
}
public void setAttribute1(String attribute1) {
this.attribute1 = attribute1;
}
public String getAttribute2() {
return attribute2;
}
public void setAttribute2(String attribute2) {
this.attribute2 = attribute2;
}
#Override
public String toString() {
return "MyBean: <attribute1: " + attribute1 + " | " + "attribute2: " + attribute2 + ">";
}
}
Outputs:
MyBean: <attribute1: value1 | attribute2: value2>
and
{96685213-a25c-4678-9a13-abd9ec81cf35={0=MyBean: <attribute1: value1 | attribute2: value2>}, 78787888-a25c-4678-9a13-abd9ec81cf35={0=MyBean: <attribute1: value3 | attribute2: value4>}}

Skip a hierarchy when deserialising with Gson

Let's suppose we have a json like this one, that cannot be modified.
And we want do deserilise it using Gson.
{
"user": {
"some_ids": {
"useless_key": [
"22a074ff-91bf-4599-9a9e-374d3f01b6e0",
"66c8ce85-f162-4d92-a836-198a17764efa",
"d0519a9e-bfa2-446c-bb98-746136a3e513"
]
}
}
}
We want to deserialise it in a class User like this one:
public class User {
#SerializedName("some_ids")
List<String> someIds;
}
The question:
The simple solution would be to create a UselessKey wrapper class and put the someIds list in it.
But is there a way to tell Gson to skip the node useless_keyand directly deserialise the List inside someIds ?
Since you still have to mark a field supposed to be processed differently, Gson does not provide anything like that. However you can implement such behavior. The closest thing to your request is #JsonAdapter
Suppose you have
private static final String JSON = "{\n"
+ " \"user\": {\n"
+ " \"some_ids\": {\n"
+ " \"useless_key\": [\n"
+ " \"22a074ff-91bf-4599-9a9e-374d3f01b6e0\",\n"
+ " \"66c8ce85-f162-4d92-a836-198a17764efa\",\n"
+ " \"d0519a9e-bfa2-446c-bb98-746136a3e513\"\n"
+ " ]\n"
+ " }\n"
+ " }\n"
+ "}";
public static void main(final String... args) {
final Gson gson = new Gson();
final Response response = gson.fromJson(JSON, Response.class);
out.println(response.getUser().getSomeIds());
}
The DTO Response class is defined as the follows:
final class Response {
private Response() { }
#SerializedName("user")
private final User user = null;
User getUser() { return user; }
static final class User {
private User() { }
#SerializedName("some_ids")
#JsonAdapter(IdsTypeAdapter.class)
private final List<String> someIds = null;
List<String> getSomeIds() { return someIds; }
}
}
The type adapter specified in #JsonAdapter(IdsTypeAdapter.class) above can be implemented as follows:
final class IdsTypeAdapter
extends TypeAdapter<List<String>> {
private static final String USELESS_PROPERTY = "useless_key";
private IdsTypeAdapter() {
}
#Override
public void write(final JsonWriter writer, final List<String> value) {
throw new UnsupportedOperationException();
}
#Override
public List<String> read(final JsonReader reader)
throws IOException {
reader.beginObject();
if ( !reader.nextName().equals(USELESS_PROPERTY) ) {
throw new UnsupportedOperationException("Expected: " + USELESS_PROPERTY);
}
reader.beginArray();
final List<String> ids = new ArrayList<>();
while ( reader.peek() == STRING ) {
ids.add(reader.nextString());
}
reader.endArray();
reader.endObject();
return unmodifiableList(ids);
}
}
The type adapter above is pretty easy, and promotes stream reading in order to improve performance (type adapters are also required by the #JsonAdapter annotation). And the result:
[22a074ff-91bf-4599-9a9e-374d3f01b6e0, 66c8ce85-f162-4d92-a836-198a17764efa, d0519a9e-bfa2-446c-bb98-746136a3e513]
Another option is use of JSON deserializers (can be registered in GsonBuilder), but the latter have performance impacts since they require the whole JSON tree to be built before a deserialization process begins. Another issue with JSON deserializers is that Gson does not support custom annotations, so in order to mark "special" fields you still need to create a wrapper class like class StringIds extends ArrayList<String> that later would even require a deserialization context to deserialize a given JsonElement to List<String> and then remapped back to StringIds. That's expensive. I would go with type adapters.
Just don't create the variable and getter and setter in your Model class. It will not then parse the key which is not found.

Parse JSON with Gson

How can I parse the JSON below with Gson?
Now I use:
private AttachChildDataModel parseSuccess(String content){
Gson gson = new Gson();
return gson.fromJson(content, AttachChildDataModel.class);
}
Where AttachChildDataModel has these member variables:
private Integer adultId;
private Integer childId;
private PlatformEnum platform;
private String regId;
private Date loginDate;
private Date logoutDate;
private ClientApp clientApp;
The Json string I'm trying to parse is:
{"log":
{
"childId":2,
"adultId":1,
"logoutDate":null,
"platform":"IPHONE",
"regId":null,
"loginDate":1325419200000,
"clientApp":"CHILD_APP"
}
}
When I put the object into the Spring ModelView, I add it under name log. The problematic thing is when I try to parse it with Gson. Right now, I manually remove the "log" prefix and the "}" postfix with String#substring, but I think there's a better solution.
To solve your problem, just create a "foo" class like this:
package stackoverflow.questions.q15614008;
public class Foo {
public AttachChildDataModel log;
}
and use it as base class for parsing in Gson:
package stackoverflow.questions.q15614008;
import com.google.gson.*;
public class Q15614008 {
public static void main(String[] arg) {
String testString = "{\"log\": "
+ " {"
+ "\"childId\":2," + "\"adultId\":1,"
+ "\"logoutDate\":null,"
+ "\"platform\":\"IPHONE\","
+ "\"regId\":null,"
+ "\"loginDate\":1325419200000,"
+ "\"clientApp\":\"CHILD_APP\"}"
+ "}";
Gson gson = new Gson();
Foo foo = gson.fromJson(
testString, Foo.class);
System.out.println("Result: " + foo.log.toString());
}
}
Then use only the log member variable of the Foo class.

Categories

Resources