I am trying to de-serialize my json string to my custom class ,I want to make the
#SerializedName annotation value to accessed as configurable parameter which should read values from application properties file .I have provided the below code snippet ,But it is accepting a constant string, is there any alternative way to make these parameter's as configurable
{
FName: "Sample",
LName: "LName"}
class Test{
#SerializedName(value=${"${name}"})
private string name;
#SerializedName(value=${"${data}"})
private string data;
}
application.properties file
name=FName
data=LName
Gson is not aware of any other library unless you tell it to do so (why pay for what you might never use otherwise?). You can extend Gson on top of what Gson provides using a custom implementation of the FieldNamingStrategy interface:
public final class ValueFieldNamingStrategy
implements FieldNamingStrategy {
private final Environment environment;
private ValueFieldNamingStrategy(final Environment environment) {
this.environment = environment;
}
public static FieldNamingStrategy of(final Environment environment) {
return new ValueFieldNamingStrategy(environment);
}
#Override
public String translateName(final Field f) {
#Nullable
final Value valueAnnotation = f.getAnnotation(Value.class);
if ( valueAnnotation == null ) {
return f.getName();
}
return environment.resolvePlaceholders(valueAnnotation.value());
}
}
public final class ValueFieldNamingStrategyTest {
#org.junit.jupiter.api.Test
public void test()
throws IOException {
final ConfigurableEnvironment configurableEnvironment = new StandardEnvironment();
final MutablePropertySources propertySources = configurableEnvironment.getPropertySources();
final PropertySource<?> propertySource = new MapPropertySource("test", ImmutableMap.of(
"name", "FName",
"data", "LName"
));
propertySources.addLast(propertySource);
final FieldNamingStrategy unit = ValueFieldNamingStrategy.of(configurableEnvironment);
final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.setFieldNamingStrategy(unit)
.create();
try ( final JsonReader jsonReader = /*...open JSON reader here...*/ ) {
final Test test = gson.fromJson(jsonReader, Test.class);
Assertions.assertEquals("FName", test.name);
Assertions.assertEquals("LName", test.data);
}
}
private static final class Test {
#Value("${name}")
private String name;
#Value("${data}")
private String data;
}
}
Related
How to configure Gson to do additional processing on the value for toJson?
public class MyClass{
#SerializedName("qwerty")
#Mask(exposeFront=2, exposeRear=2, mask="*")
private String qwerty
}
Assuming MyClass#qwerty has a value of 1234567890, how to set Gson to output {"qwerty":"12******90"}?
Gson ReflectiveTypeAdapterFactory, that is responsible for "plain" objects serialization and deserialization, is not possible to enhance to support any other annotations like #Masked. It can only use annotations like #Expose (indirectly via an exclusion strategy), #SerializedName and a few others like #Since and #Until (exclusion strategy too). Note these annotations are documented and supported by default. In general, Gson suggests using a type adapter for the declaring class, MyClass, but this also means that you must manage all fields and make sure the corresponding type adapter is updated once your class is changed. Even worse, adding a custom type adapter makes these annotations support lost.
As an another way of working around it is injecting a special string type adapter factory that can do the trick, but due to the mechanics of how it is injected, this is both limited and requires duplicating the #Masked annotation values (if you're using the annotation elsewhere in your code) and the type adapter factory configuration in #JsonAdapter.
public abstract class MaskedTypeAdapterFactory
implements TypeAdapterFactory {
private final int exposeFront;
private final int exposeRear;
private final char mask;
private MaskedTypeAdapterFactory(final int exposeFront, final int exposeRear, final char mask) {
this.exposeFront = exposeFront;
this.exposeRear = exposeRear;
this.mask = mask;
}
// must be "baked" into the class (name only represents the configuration)
public static final class _2_2_asterisk
extends MaskedTypeAdapterFactory {
private _2_2_asterisk() {
super(2, 2, '*');
}
}
#Override
#Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( typeToken.getRawType() != String.class ) {
return null;
}
#SuppressWarnings("unchecked")
final TypeAdapter<String> delegate = (TypeAdapter<String>) gson.getAdapter(typeToken);
final TypeAdapter<String> typeAdapter = new TypeAdapter<String>() {
#Override
public void write(final JsonWriter out, final String value)
throws IOException {
// mask the value
final int length = value.length();
final char[] buffer = value.toCharArray();
for ( int i = exposeFront; i < length - exposeRear; i++ ) {
buffer[i] = mask;
}
out.value(new String(buffer));
}
#Override
public String read(final JsonReader in)
throws IOException {
return delegate.read(in);
}
}
.nullSafe();
#SuppressWarnings("unchecked")
final TypeAdapter<T> adapter = (TypeAdapter<T>) typeAdapter;
return adapter;
}
}
#NoArgsConstructor
#AllArgsConstructor
final class MyClass {
#SerializedName("qwerty")
#Mask(exposeFront = 2, exposeRear = 2, mask = "*")
// unfortunately, this must duplicate the #Mask annotation values
// since type adapter (factories) do not accept supplemental information
// and Java annotations can only accept compile-time constants
#JsonAdapter(MaskedTypeAdapterFactory._2_2_asterisk.class)
#SuppressWarnings("unused")
private String qwerty;
}
Test:
public final class MaskedTypeAdapterFactoryTest {
private static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.create();
#Test
public void test() {
final String actual = gson.toJson(new MyClass("1234567890"));
final String expected = "{\"qwerty\":\"12******90\"}";
Assertions.assertEquals(expected, actual);
}
}
This is probably the most robust way of doing that in Gson.
I'm trying to figure out how to combine the code generator I use in my project and Jackson so that I could combine them both.
The third-party bean code generator does some things that I would like to improve.
For example, the class below
public class Wrapper {
public String string;
public List<String> array;
}
does not have default values set for both string and array.
Under some circumstances (and mostly due to heavy legacy reasons) I'd like Jackson to deserialize the above bean class with the default values set if they are not provided in the input JSON document.
For example, I'd like {"string": "foo"} to be deserialized to a bean as if the source JSON were {"string":"foo","array":[]} so that it would result in a bean with two non-null fields.
The first idea I came up with is creating a bean instance, then run a "set default fields" preprocessor, and then read the JSON into the constructed and initialized bean.
public final class DefaultsModule
extends SimpleModule {
#Override
public void setupModule(final SetupContext setupContext) {
setupContext.addBeanDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(final DeserializationConfig config, final BeanDescription description,
final JsonDeserializer<?> defaultDeserializer) {
return DefaultFieldsJsonDeserializer.create(description.getType(), description);
}
});
}
private static final class DefaultFieldsJsonDeserializer<T>
extends JsonDeserializer<T> {
// the generated classes set is finite, so won't bother with subclassing
private static final Map<Class<?>, Supplier<?>> NEW_INSTANCES = new ImmutableMap.Builder<Class<?>, Supplier<?>>()
.put(Iterable.class, ArrayList::new)
.put(Collection.class, ArrayList::new)
.put(List.class, ArrayList::new)
.put(ArrayList.class, ArrayList::new)
.put(LinkedList.class, LinkedHashMap::new)
.put(Map.class, LinkedHashMap::new)
.put(HashMap.class, HashMap::new)
.put(LinkedHashMap.class, LinkedHashMap::new)
.put(TreeMap.class, TreeMap::new)
.put(Set.class, LinkedHashSet::new)
.put(HashSet.class, HashSet::new)
.put(LinkedHashSet.class, LinkedHashSet::new)
.put(TreeSet.class, TreeSet::new)
.build();
private final BeanDescription description;
private final Iterable<? extends Map.Entry<Field, ? extends Supplier<?>>> fieldDefaultsChain;
private DefaultFieldsJsonDeserializer(final BeanDescription description,
final Iterable<? extends Map.Entry<Field, ? extends Supplier<?>>> fieldDefaultsChain) {
this.description = description;
this.fieldDefaultsChain = fieldDefaultsChain;
}
private static <T> JsonDeserializer<T> create(final JavaType javaType, final BeanDescription description) {
final Iterable<? extends Map.Entry<Field, ? extends Supplier<?>>> fieldDefaultsChain = Stream.of(javaType.getRawClass().getDeclaredFields())
.filter(field -> NEW_INSTANCES.containsKey(field.getType()))
.peek(field -> field.setAccessible(true))
.map(field -> new AbstractMap.SimpleImmutableEntry<Field, Supplier<Object>>(field, () -> NEW_INSTANCES.get(field.getType()).get()))
.collect(Collectors.toList());
return new DefaultFieldsJsonDeserializer<>(description, fieldDefaultsChain);
}
#Override
#Nullable
public T deserialize(final JsonParser parser, final DeserializationContext context)
throws IOException {
try {
// instantiate the bean
#Nullable
#SuppressWarnings("unchecked")
final T bean = (T) description.instantiateBean(false);
if ( bean == null ) {
return null;
}
// do default values pre-processing
for ( final Map.Entry<Field, ? extends Supplier<?>> e : fieldDefaultsChain ) {
final Field field = e.getKey();
final Object defaultValue = e.getValue().get();
field.set(bean, defaultValue);
}
// since the object is constructed and initialized properly, simply update it
final ObjectReader objectReader = ((ObjectMapper) parser.getCodec())
.readerForUpdating(bean);
return objectReader.readValue(parser);
} catch ( final IllegalAccessException ex ) {
return context.reportBadTypeDefinition(description, ex.getMessage());
}
}
}
}
In short, I'd like the following unit test to pass:
public final class DefaultsModuleTest {
#NoArgsConstructor
#AllArgsConstructor
#EqualsAndHashCode
private static final class Generated {
#JsonProperty
private String string;
#JsonProperty
private List<String> array /*not generated but should be initialized in the pre-processor = new ArrayList<>()*/;
}
#Test
public void test()
throws IOException {
final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new DefaultsModule());
final Generated expected = new Generated("foo", Collections.emptyList());
Assertions.assertEquals(expected, objectMapper.readValue("{\"string\":\"foo\"}", Generated.class));
Assertions.assertEquals(expected, objectMapper.readValue("{\"string\":\"foo\",\"array\":null}", Generated.class));
Assertions.assertEquals(expected, objectMapper.readValue("{\"string\":\"foo\",\"array\":[]}", Generated.class));
}
}
Unfortunately, the deserializer above runs in the infinite recursion loop.
So I have multiple questions:
how to implement it properly?
maybe I should go with ValueInstantiator somehow?
what is a generic way to get the delegate JSON deserializer? (Gson allows to obtain delegate type adapters in type adapter factories, Jackson offers the deserializer modifier approach but the JsonDeserializer coming in the modifier causes weird exceptions + not sure if it can update existing objects).
My Jackson databind version is 2.9.10.
I seem to have realized the way it had to be done properly. I didn't notice that I can add value instantiators I mentioned above to the module setup context. Having it configured, I simply don't need to create a custom deserializer since I can supply constructed+initialized values myself.
public final class DefaultsModule
extends SimpleModule {
#Override
public void setupModule(final SetupContext setupContext) {
setupContext.addValueInstantiators((config, description, defaultInstantiator) -> DefaultFieldsInstantiator.isSupported(description.getBeanClass())
? DefaultFieldsInstantiator.create(config, description)
: defaultInstantiator
);
}
private static final class DefaultFieldsInstantiator
extends StdValueInstantiator {
private static final Map<Class<?>, Supplier<?>> NEW_INSTANCES = new ImmutableMap.Builder<Class<?>, Supplier<?>>()
.put(Iterable.class, ArrayList::new)
.put(Collection.class, ArrayList::new)
.put(List.class, ArrayList::new)
.put(ArrayList.class, ArrayList::new)
.put(LinkedList.class, LinkedHashMap::new)
.put(Map.class, LinkedHashMap::new)
.put(HashMap.class, HashMap::new)
.put(LinkedHashMap.class, LinkedHashMap::new)
.put(TreeMap.class, TreeMap::new)
.put(Set.class, LinkedHashSet::new)
.put(HashSet.class, HashSet::new)
.put(LinkedHashSet.class, LinkedHashSet::new)
.put(TreeSet.class, TreeSet::new)
.build();
private final BeanDescription description;
private final Iterable<? extends Map.Entry<Field, ? extends Supplier<?>>> fieldDefaultsChain;
private DefaultFieldsInstantiator(final DeserializationConfig config, final BeanDescription description,
final Iterable<? extends Map.Entry<Field, ? extends Supplier<?>>> fieldDefaultsChain) {
super(config, description.getType());
this.description = description;
this.fieldDefaultsChain = fieldDefaultsChain;
}
private static boolean isSupported(final Class<?> clazz) {
return ...............;
}
private static ValueInstantiator create(final DeserializationConfig config, final BeanDescription description) {
final Iterable<? extends Map.Entry<Field, ? extends Supplier<?>>> fieldDefaultsChain = Stream.of(description.getType().getRawClass().getDeclaredFields())
.filter(field -> NEW_INSTANCES.containsKey(field.getType()))
.peek(field -> field.setAccessible(true))
.map(field -> new AbstractMap.SimpleImmutableEntry<Field, Supplier<Object>>(field, () -> NEW_INSTANCES.get(field.getType()).get()))
.collect(Collectors.toList());
return new DefaultFieldsInstantiator(config, description, fieldDefaultsChain);
}
#Override
public boolean canCreateUsingDefault() {
return true;
}
#Override
#Nullable
public Object createUsingDefault(final DeserializationContext context)
throws JsonMappingException {
try {
#Nullable
final Object bean = description.instantiateBean(false);
if ( bean == null ) {
return null;
}
for ( final Map.Entry<Field, ? extends Supplier<?>> e : fieldDefaultsChain ) {
final Field field = e.getKey();
final Object defaultValue = e.getValue().get();
field.set(bean, defaultValue);
}
return bean;
} catch ( final IllegalAccessException ex ) {
return context.reportBadDefinition(description.getType(), "Cannot set field: " + ex.getMessage());
}
}
}
}
And all the following tests pass:
final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new DefaultsModule());
Assertions.assertNull(objectMapper.readValue("null", Generated.class));
Assertions.assertEquals(new Generated(null, Collections.emptyList()), objectMapper.readValue("{}", Generated.class));
Assertions.assertEquals(new Generated(null, ImmutableList.of("bar")), objectMapper.readValue("{\"array\":[\"bar\"]}", Generated.class));
Assertions.assertEquals(new Generated("foo", Collections.emptyList()), objectMapper.readValue("{\"string\":\"foo\"}", Generated.class));
Assertions.assertEquals(new Generated("foo", null), objectMapper.readValue("{\"string\":\"foo\",\"array\":null}", Generated.class));
Assertions.assertEquals(new Generated("foo", Collections.emptyList()), objectMapper.readValue("{\"string\":\"foo\",\"array\":[]}", Generated.class));
I am trying to develop an AWS Lambda function that is triggered by events from SQS.
I am using the spring-cloud-function-adapter-aws (version 1.0.0.RELEASE) and in specifically a SpringBootRequestHandler.
However, the ObjectMapper that is being used is case-sensitive and therefore failing to successful convert the Json coming from SQS.
SQS publishes the following Json and it is the Records field in particular that I'm having the problem with.
{
"Records": [
{
"body": "Hello from SQS!",
"receiptHandle": "MessageReceiptHandle",
"md5OfBody": "7b270e59b47ff90a553787216d55d91d",
"eventSourceARN": "arn:aws:sqs:eu-west-1:123456789012:MyQueue",
"eventSource": "aws:sqs",
"awsRegion": "eu-west-1",
"messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78",
"attributes": {
"ApproximateFirstReceiveTimestamp": "1523232000001",
"SenderId": "123456789012",
"ApproximateReceiveCount": "1",
"SentTimestamp": "1523232000000"
},
"messageAttributes": {}
}
]
}
I have tried the suggestions in this question, but to no avail. Configuring ObjectMapper in Spring
In my POJO, I've also added the below annotation but it isn't working either whilst it would outside of Lambda.
#JsonProperty("Records")
private List<SqsRecord> Records;
Any help would be much appreciated.
My Lambda handler is defined as:
public class SqsEventHandler extends SpringBootRequestHandler<SqsEvent, String> {}
The POJO defined as:
public class SqsEvent {
#JsonProperty("Records")
private List<SqsRecord> records;
#Data
public class SqsRecord {
private String body;
private String receiptHandle;
private String md5OfBody;
private String eventSourceARN;
private String eventSource;
private String awsRegion;
private String messageId;
}
}
I expect the Json from the sample message to be able to be read in by the ObjectMapper, but the field "records" is null.
I got this issue solved in a more simple manner.
Referencing https://docs.aws.amazon.com/lambda/latest/dg/java-handler-io-type-stream.html and in specific
if Lambda's serialization approach does not meet your needs, you can use the byte stream implementation
I am now using the SpringBootStreamHandler directly and I have created an ObjectMapper instance with my required configuration options in my Spring Configuration class as:
#Bean
public ObjectMapper objectMapper() {
final ObjectMapper mapper = new ObjectMapper();
mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper;
}
we've got this problem with a lot of AWS services.
You must define a new mapper like this :
SQSMixin :
private static interface SQSEventMixin {
public static final String ATTRIBUTES = "attributes";
public static final String AWS_REGION = "awsRegion";
public static final String BODY = "body";
public static final String EVENT_SOURCE = "eventSource";
public static final String EVENT_SOURCE_ARN = "eventSourceARN";
public static final String MD5_OF_BOBY = "md5OfBody";
public static final String MD5_OF_MESSAGE_ATTRIBUTES = "md5OfMessageAttributes";
public static final String MESSAGE_ID = "messageId";
public static final String RECEIPT_HANDLE = "receiptHandle";
#JsonProperty(value = "Records")
public List<?> getRecords();
static interface MessageMixin {
#JsonProperty(ATTRIBUTES)
public String getAttributes();
#JsonProperty(ATTRIBUTES)
public void setAttributes(String attributes);
#JsonProperty(AWS_REGION)
public String getAwsRegion();
#JsonProperty(AWS_REGION)
public void setAwsRegion(String awsRegion);
#JsonProperty(BODY)
public Object getBody();
#JsonProperty(BODY)
public void setBody(Object body);
#JsonProperty(EVENT_SOURCE)
public String getEventSource();
#JsonProperty(EVENT_SOURCE)
public void setEventSource(String eventSource);
#JsonProperty(EVENT_SOURCE_ARN)
public String getEventSourceArn();
#JsonProperty(EVENT_SOURCE_ARN)
public void setEventSourceArn(String eventSourceArn);
#JsonProperty(MD5_OF_BOBY)
public String getMd5OfBody();
#JsonProperty(MD5_OF_BOBY)
public void setMd5OfBody(String md5OfBody);
#JsonProperty(MD5_OF_MESSAGE_ATTRIBUTES)
public String getMd5OfMessageAttributes();
#JsonProperty(MD5_OF_MESSAGE_ATTRIBUTES)
public void setMd5OfMessageAttributes(String md5OfMessageAttributes);
#JsonProperty(MESSAGE_ID)
public String getMessageId();
#JsonProperty(MESSAGE_ID)
public void setMessageId(String messageId);
#JsonProperty(RECEIPT_HANDLE)
public String getReceiptHandle();
#JsonProperty(RECEIPT_HANDLE)
public void setReceiptHandle(String receiptHandle);
}
}
A Strategy for record :
private static class UpperCaseRecordsPropertyNamingStrategy extends PropertyNamingStrategy.PropertyNamingStrategyBase {
private static final long serialVersionUID = 1L;
#Override
public String translate(String propertyName) {
if (propertyName.equals("records")) {
return "Records";
}
return propertyName;
}
}
Formatter for Date :
private static final DateTimeFormatter dateTimeFormatter = ISODateTimeFormat.dateTime()
.withZone(new FixedDateTimeZone("GMT", "GMT", 0, 0));
private static class DateTimeMapperModule extends SimpleModule {
private static final long serialVersionUID = 1L;
public DateTimeMapperModule() {
super("DateTimeMapperModule");
super.addSerializer(DateTime.class, new DateTimeSerializer());
super.addDeserializer(DateTime.class, new DateTimeDeserializer());
}
}
private static class DateTimeSerializer extends JsonSerializer<DateTime> {
#Override
public void serialize(DateTime value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeString(dateTimeFormatter.print(value));
}
}
private static class DateTimeDeserializer extends JsonDeserializer<DateTime> {
#Override
public DateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException {
return dateTimeFormatter.parseDateTime(parser.getText());
}
}
And declare your mapper :
ObjectMapper mapper = new ObjectMapper();
mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
mapper.setPropertyNamingStrategy(new UpperCaseRecordsPropertyNamingStrategy());
mapper.registerModule(new DateTimeMapperModule());
mapper.addMixIn(SQSMessage.class, SQSEventMixin.MessageMixin.class);
SQSEvent request = mapper.convertValue(inputObject, SQSEvent.class);
There is already an official library that is supporting this: https://aws.amazon.com/blogs/opensource/testing-aws-lambda-functions-written-in-java/
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-tests</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>
Also have surefire in your plugins:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</build>
Example:
SQSEvent input = EventLoader.loadEvent("/sqsEvent.json", SQSEvent.class);
How to set super class "LookUp" properties id, name when parsing JSON array
of countries:
[ {"ID":5, "CountryNameEN":"UK" }, {"ID":6, "CountryNameEN":"USA" } ]
For example, When i calling get_lookups_countries() API with Retrofit 2 & parse the response with google Gson Library, I want to set super class instance members id & name with the same values of derived class "Country"
#GET(Constants.LookUps.GET_COUNTRIES) Call<List<Country>> get_lookups_countries();
Gson gson = new GsonBuilder()
.setLenient()
.registerTypeAdapter(LookUp.class,new LookupsDeserializer())
.create();
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient.Builder okHttpClient = new OkHttpClient.Builder();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient.build())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
return retrofit.create(APIEndpointVatTax.class);
public class LookUp {
int id;
String name;
}
public class Country extends LookUp {
#SerializedName("ID")
#Expose
private Integer iD;
#SerializedName("CountryNameEN")
#Expose
private String countryNameEN;
}
You seem to have some issues with your JSON mappings: you're trying to bind the super class fields to the sub class fields, however this is where an interface might be a better choice for you, because your intention is just asking the deserialized object for its id and name.
I would do it like this:
interface LookUp {
int getId();
String getName();
}
final class CountryByInterface
implements LookUp {
#SerializedName("ID")
private final Integer id = null;
#SerializedName("CountryNameEN")
private final String name = null;
#Override
public int getId() {
return id;
}
#Override
public String getName() {
return name;
}
}
So it could be used easily (Java 8 for the demo purposes only):
final Gson gson = new Gson();
final Type countryListType = new TypeToken<List<CountryByInterface>>() {
}.getType();
try ( final Reader reader = getPackageResourceReader(Q43247712.class, "countries.json") ) {
gson.<List<CountryByInterface>>fromJson(reader, countryListType)
.stream()
.map(c -> c.getId() + "=>" + c.getName())
.forEach(System.out::println);
}
If for some justified reason you really need the super class to hold such fields, you have to implement a post-processor (inspired with PostConstructAdapterFactory). Say,
abstract class AbstractLookUp {
int id;
String name;
abstract int getId();
abstract String getName();
final void postSetUp() {
id = getId();
name = getName();
}
}
final class CountryByClass
extends AbstractLookUp {
#SerializedName("ID")
private final Integer id = null;
#SerializedName("CountryNameEN")
private final String name = null;
#Override
int getId() {
return id;
}
#Override
String getName() {
return name;
}
}
final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new TypeAdapterFactory() {
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// Check if it's a class we can handle: AbstractLookUp
if ( AbstractLookUp.class.isAssignableFrom(typeToken.getRawType()) ) {
// Get the downstream parser for the given type
final TypeAdapter<T> delegateTypeAdapter = gson.getDelegateAdapter(this, typeToken);
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, final T value)
throws IOException {
delegateTypeAdapter.write(out, value);
}
#Override
public T read(final JsonReader in)
throws IOException {
// Deserialize it as an AbstractLookUp instance
final AbstractLookUp abstractLookUp = (AbstractLookUp) delegateTypeAdapter.read(in);
// And set it up
abstractLookUp.postSetUp();
#SuppressWarnings("unchecked")
final T result = (T) abstractLookUp;
return result;
}
};
}
return null;
}
})
.create();
final Type countryListType = new TypeToken<List<CountryByClass>>() {
}.getType();
try ( final Reader reader = getPackageResourceReader(Q43247712.class, "countries.json") ) {
gson.<List<CountryByClass>>fromJson(reader, countryListType)
.stream()
.map(c -> ((AbstractLookUp) c).id + "=>" + ((AbstractLookUp) c).name)
.forEach(System.out::println);
}
Both examples produce
5=>UK
6=>USA
However I find the first approach better designed and much easier to use, whereas the second one demonstrates how Gson can be configured to implement complex (de)serialization strategies.
I have JSON, with differents levels field, so I want to convert to a single JSON with fields with one level for example:
{
"prop1":"value1",
"prob2":"value2",
"prop3": {
"prop4":"value4",
"prop5":"value5"
}
... many level fields
}
result
{
"prop1":"value1",
"prop2":"value2",
"prop4":"value4",
"prop5":"value5"
.......
}
I'm using Jackson with annotation #JsonProperty("field"), I haven't problem wih fields of first level , but I donĀ“t know how to access field where to into more inside JSON , for this example are prop4 and prop5.
JsonUnwrapped is the annotation to use, it even works for multi-level nesting. For example:
#RunWith(JUnit4.class)
public class Sample {
#Test
public void testName() throws Exception {
SampleClass sample = new SampleClass("value1", "value2", new SubClass("value4", "value5", new SubSubClass("value7")));
new ObjectMapper().writeValue(System.out, sample);
}
#JsonAutoDetect(fieldVisibility=Visibility.ANY)
public static class SampleClass {
private String prop1;
private String prop2;
#JsonUnwrapped
private SubClass prop3;
public SampleClass(String prop1, String prop2, SubClass prop3) {
this.prop1 = prop1;
this.prop2 = prop2;
this.prop3 = prop3;
}
}
#JsonAutoDetect(fieldVisibility=Visibility.ANY)
public static class SubClass {
private String prop4;
private String prop5;
#JsonUnwrapped
private SubSubClass prop6;
public SubClass(String prop4, String prop5, SubSubClass prop6) {
this.prop4 = prop4;
this.prop5 = prop5;
this.prop6 = prop6;
}
}
#JsonAutoDetect(fieldVisibility=Visibility.ANY)
public static class SubSubClass{
private String prop7;
public SubSubClass(String prop7) {
this.prop7 = prop7;
}
}
}
will generate
{"prop1":"value1","prop2":"value2","prop4":"value4","prop5":"value5","prop7":"value7"}
Try implementing the #JsonUnwrapped annotation. More information at http://jackson.codehaus.org/1.9.9/javadoc/org/codehaus/jackson/annotate/JsonUnwrapped.html