Gson Serializer for a given class if in another specific class - java

I am trying to serialize (using Gson) a POJO and to have a special treatment for a single one of its fields.
Is it possible to do it in a simpler way than coding an adapter implementing JsonSerializer and having its serialize() method copy every field except for a specific one which receives the special treatment ?
Would it even be possible to make it using annotations in my POJO ?
I also cannot just write an adapter of the type of the specific field as it is a java.util.Date and I do not want every serialized Date to receive this treatment.
Here is an illustration :
public class Pojo {
#SerializedName("effectiveDate")
private final Date mDate;
#SerializedName("status")
private final Status mStatus; // <-- The field needing specific serialization
#SerializedName("details")
private final String mDetails;
// other fields
// methods
}
I would like to avoid coding an adapter as such :
public class PojoAdapter implements JsonSerializer<Pojo> {
#Override
public JsonElement serialize(final Pojo src, final Type typeOfSrc, final JsonSerializationContext context) {
final JsonObject jsonPojo = new JsonObject();
jsonDeployment.add("effectiveDate", /* special treatment */);
jsonDeployment.add("status", src.getStatus());
jsonDeployment.add("details", src.getDetails());
// other fields setting
return jsonPojo;
}
}

You can implement custom com.google.gson.JsonSerializer for a Date class and use com.google.gson.annotations.JsonAdapte annotation for given field to register it. See below example:
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import java.lang.reflect.Type;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
public class GsonApp {
public static void main(String[] args) {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
System.out.println(gson.toJson(new DatesPojo(new Date())));
}
}
class CustomDateJsonSerializer implements JsonSerializer<Date> {
#Override
public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
String format = src.toInstant().atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ISO_TIME);
return new JsonPrimitive(format + " ISO TIME");
}
}
class DatesPojo {
#JsonAdapter(CustomDateJsonSerializer.class)
#SerializedName("customDate")
private final Date mDate0;
#SerializedName("effectiveDate")
private final Date mDate1;
public DatesPojo(Date mDate) {
this.mDate0 = mDate;
this.mDate1 = mDate;
}
public Date getmDate0() {
return mDate0;
}
public Date getmDate1() {
return mDate1;
}
}
Above code prints:
{
"customDate": "22:37:21.806+01:00 ISO TIME",
"effectiveDate": "Jan 22, 2020 10:37:21 PM"
}

I found another solution which consists in making my Date field implement an EffectiveDate interface which just extends Date and to add an adapter for this single field.

Related

Deserialize to String or Object using Jackson

I have an object that sometimes looks like this:
{
"foo" : "bar",
"fuzz" : "bla"
}
and sometimes looks like this:
{
"foo" : { "value" : "bar", "baz": "asdf" },
"fuzz" : { "thing" : "bla", "blip" : "asdf" }
}
these classes would look like:
public class Foo {
String value;
String baz;
}
public class Fuzz {
String thing;
String blip;
}
where the first cases are shorthand for the second ones. I would like to always deserialize into the second case.
Further - this is a pretty common pattern in our code, so I would like to be able to do the serialization in a generic manner, as there are other classes similar to Foo above that have the same pattern of using String as a syntactic sugar for a more complex object.
I'd imagine the code to use it would look something like this
public class Thing {
#JsonProperty("fuzz")
Fuzz fuzz;
#JsonProperty("foo")
Foo foo;
}
How do I write a custom deserializer (or some other module) that generically handles both cases?
To make it generic we need to be able to specify name which we would like to set in object for JSON primitive. Some flexibility gives annotation approach. Let's define simple annotation:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.FIELD)
#interface JsonPrimitiveName {
String value();
}
Name means: in case primitive will appear in JSON use value() to get property name for given primitive. It binds JSON primitive with POJO field. Simple deserialiser which handles JSON object and JSON primitive:
class PrimitiveOrPojoJsonDeserializer extends JsonDeserializer implements ContextualDeserializer {
private String primitiveName;
private JavaType type;
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonDeserializer<Object> deserializer = ctxt.findRootValueDeserializer(type);
if (p.currentToken() == JsonToken.START_OBJECT) {
return deserializer.deserialize(p, ctxt);
} else if (p.currentToken() == JsonToken.VALUE_STRING) {
BeanDeserializer beanDeserializer = (BeanDeserializer) deserializer;
try {
Object instance = beanDeserializer.getValueInstantiator().getDefaultCreator().call();
SettableBeanProperty property = beanDeserializer.findProperty(primitiveName);
property.deserializeAndSet(p, ctxt, instance);
return instance;
} catch (Exception e) {
throw JsonMappingException.from(p, e.getMessage());
}
}
return null;
}
#Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
JsonPrimitiveName annotation = property.getAnnotation(JsonPrimitiveName.class);
PrimitiveOrPojoJsonDeserializer deserializer = new PrimitiveOrPojoJsonDeserializer();
deserializer.primitiveName = annotation.value();
deserializer.type = property.getType();
return deserializer;
}
}
Now we need to annotate POJO fields as below:
class Root {
#JsonPrimitiveName("value")
#JsonDeserialize(using = PrimitiveOrPojoJsonDeserializer.class)
private Foo foo;
#JsonPrimitiveName("thing")
#JsonDeserialize(using = PrimitiveOrPojoJsonDeserializer.class)
private Fuzz fuzz;
// getters, setters
}
I assume that all classes are POJO-s and follow all rules - have getters, setters and default constructor. In case constructor does not exist you need to change this beanDeserializer.getValueInstantiator().getDefaultCreator().call() line somehow which fits your requirements.
Example app:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public class JsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.readValue(jsonFile, Root.class));
}
}
Prints for shortened JSON:
Root{foo=Foo{value='bar', baz='null'}, fuzz=Fuzz{thing='bla', blip='null'}}
And for full JSON payload:
Root{foo=Foo{value='bar', baz='asdf'}, fuzz=Fuzz{thing='bla', blip='asdf'}}

Deserialize json with boolean or object field

I use Retrofit and GSON and I'm getting Json like this:
{
"firstName": "John",
"lastName": "Doe",
"passporRf": {
"number": "996633",
"series": "1111",
"code": "66666"
}
}
And when user doenst have a passport - this fields is boolean with "false" value.
How to deserialize it correctly and get boolean value - false, when this field is boolean and get JSON object when its object.
I found a JSONDeserializer but i cant to use it correctly. Code is:
public class DocumentDeserializer implements JsonDeserializer<Passport> {
#Override
public Passport deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonPrimitive primitive = json.getAsJsonPrimitive();
if (primitive.isBoolean()) {
// What value do i have to return here?
}
return context.deserialize(json, Passport.class);
}
}
Don't use GSon nor any other binding library. Nothing can be bound to "either a boolean or an object", so don't try.
Use a JSON parser, and look at the JSON content to obtain the information you want. Possibly build a User object out of the information you can find.
Code is given below,
Sample.class
import com.google.gson.Gson;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.List;
/**
* Class containing news information in form of POJO.
*/
public class Sample {
public String firstName;
public String lastName;
private List<SampleOne> passporRf;
public List<SampleOne> getPassporRf() {
return passporRf;
}
private void setPassporRf(List<SampleOne> passporRf) {
this.passporRf = passporRf;
}
// Using custom DeSerializer for "multimedia" object since the API is just "Great"
public static class SampleDetailsDeSerializer implements JsonDeserializer<Sample> {
#Override
public Sample deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
Sample accountState = new Gson().fromJson(json, Sample.class);
JsonObject jsonObject = json.getAsJsonObject();
if (jsonObject.has("passporRf")) {
JsonElement elem = jsonObject.get("passporRf");
if (elem != null && !elem.isJsonNull()) {
if(elem.isJsonPrimitive()){
accountState.setPassporRf(null);
} else {
final List<SampleOne> passporRfList = new Gson().fromJson(elem.getAsJsonArray().toString()
, new TypeToken<List<SampleOne>>(){}.getType());
accountState.setPassporRf(passporRfList);
}
}
}
return accountState ;
}
}
}
SampleOne.class
public class SampleOne {
public String number;
public String series;
public String code;
}
Hope it may help you.

Check if json property is present, is string, and is correct value

Using gson, I use this cumbersome approach to make sure a required property has a desired value:
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
public class ScratchSpace {
public static void main(String[] args) {
// create json object from source data - in my real code, this is sourced externally
JsonObject json = new JsonParser().parse("{ \"product\": \"foobar\"}").getAsJsonObject();
// does this object have a key called product, which is a string, and equal to our expected value?
boolean correctProduct = false;
if (json.has("product")) {
JsonElement productElement = json.get("product");
if (productElement.isJsonPrimitive()) {
String product = productElement.getAsString();
if ("foobar".equals(product)) {
correctProduct = true;
}
}
}
System.out.println("correctProduct = " + correctProduct);
}
}
I'm almost certain I'm doing this suboptimally. Is there a simple, readable, short-ish one-liner to achieve the same?
Edit: if possible, I'd like to keep using gson.
Using java.util.Optional, the following works:
final boolean correctProduct = Optional.ofNullable(json.get("product"))
.filter(JsonPrimitive.class::isInstance)
.map(JsonPrimitive.class::cast)
.map(JsonPrimitive::getAsString)
.filter("foobar"::equals)
.isPresent();
You can write a custom deserializer like this, register it, and use fromJson method to obtain object directly from json string. In this way, you can return null or throw exception in deserializer if the json string is not in expected format.
Note that you don't have to set each field seperately. After performing custom checks, you can use default deserialization from context.
EDIT: If you want to obtain just true/false instead of the complete object, then you can write a MyBoolean class, holding a simple boolean value and use fromJson method to deserialize to MyBoolean class. The custom deserializer will perform only the desired checks and set the content of MyBoolean instance appropriately. Furthermore, (I guess) if you extend this MyBoolean from Boolean, you can use it as boolean too.
EDIT 2: I didn't have to time to include a sample code before. Here is what I suggest:
package io.ram.ram;
import java.lang.reflect.Type;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
public class Tester {
public static class MyBoolean {
private boolean value;
public void set(boolean value) {
this.value = value;
}
public boolean get() {
return value;
}
}
public static class MyAdapter implements JsonDeserializer<MyBoolean> {
public MyBoolean deserialize(JsonElement json, Type type, JsonDeserializationContext context)
throws JsonParseException {
MyBoolean result = new MyBoolean();
result.set(false);
try {
result.set(json.getAsJsonObject().get("product").getAsString().equals("foobar"));
} catch (Exception e) {
}
return result;
}
}
public static void main(String[] args) {
Gson gson = new GsonBuilder().registerTypeAdapter(MyBoolean.class, new MyAdapter()).create();
System.out.println(gson.fromJson("{\"product\": \"foobar\"}", MyBoolean.class).get());
System.out.println(gson.fromJson("{\"product\": \"foobaz\"}", MyBoolean.class).get());
}
}
As I said, after you register a type adapter for custom serialization, you can achieve what you want with a single line. (Note: Boolean class is final, so we cannot extend it. Sorry for this wrong information.)
You can parametrize MyAdapter for strings "product" and "foobar" of course, thus you don't have to create such classes for every possible cases.
I know you said GSON, but the pojo based approach jackson offers makes what you want to do just too convenient to not to post:
Simple pojo:
public class FooPojo {
#JsonProperty
private String product;
public String getProduct() {
return product;
}
public void setProduct(String product) {
this.product = product;
}
public boolean isProductEqualTo(String check) {
return product.equals(check);
}
}
Parse and check:
public static void main(String[] args) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
FooPojo fooPojo = objectMapper.readValue("{ \"product\": \"foobar\"}", FooPojo.class);
System.out.println(fooPojo.isProductEqualTo("foobar"));
}

Why is my DateTime deserializer is truncating DateTime's minute/second/millisecond?

I have a class that deserializes a JSON element.
public class DateTimeConverter implements JsonSerializer<DateTime>, JsonDeserializer<DateTime>
{
private static final DateTimeFormatter DATE_FORMAT = ISODateTimeFormat.dateHourMinuteSecondMillis();
#Override
public JsonElement serialize(DateTime src, Type typeOfSrc, JsonSerializationContext context)
{
final DateTimeFormatter fmt = ISODateTimeFormat.dateHourMinuteSecondMillis();
return new JsonPrimitive(fmt.print(src));
}
#Override
public DateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException
{
final String dateAsString = json.getAsString();
System.out.println(dateAsString);
if (json.isJsonNull() || dateAsString.length()==0)
{
return null;
}
else
{
return DATE_FORMAT.parseDateTime(json.getAsString());
}
}
}
However, my Deserialize method when I input:
2015-07-29T11:00:00.000Z
I receive:
2015-07-29T11
from the System.out.println(dateAsString); Why is it truncating my input?
I think my issue is within my test class:
I constructed a DateTime object to be used with Google's Gson. However, I think the default constructor for DateTimeType doesn't support minute/second/millisecond. Is there a way I can extend the DateTimeType to support it?
Here is my test class:
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import org.joda.time.DateTime;
import org.junit.Test;
import java.lang.reflect.Type;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
/**
* Tests {#link DateTimeConverter}.
*/
public class DateTimeConverterTest {
String testTime = "2015-07-29T11:00:00.001Z";
#Test
public void testDateTimeConverter() throws Exception {
final Gson gson = initCustomGSON();
Type DateTimeType = new TypeToken<DateTime>() {
}.getType();
System.out.println(testTime);
DateTimeConverter timeConverter = new DateTimeConverter();
DateTime m = (gson.fromJson(testTime, DateTimeType));
assertThat("11", is(m.hourOfDay().getAsText()));
}
public Gson initCustomGSON() {
final GsonBuilder builder = new GsonBuilder();
JodaTimeConverters converter = new JodaTimeConverters();
converter.registerAll(builder);
return builder.create();
}
}
You have a few issues with this code.
Your first problem is that : is an operator in Json. You are interpreting an unescaped String with a : in it, so Gson is interpreting it as key : value. Your test string needs to surround the entire text date with quotes to prevent this from happening, e.g.
String testTime = "\"2015-07-29T11:00:00.001Z\"";
You were using ISODateTimeFormat.dateHourMinuteSecondMillis() in your code. However, the format pattern for this is yyyy-MM-dd'T'HH:mm:ss.SSS, which as you can see does not include a time zone. You want to be using ISODateTimeFormat.dateTime(), whose pattern is yyyy-MM-dd'T'HH:mm:ss.SSSZZ, which does have a time zone.
private static final DateTimeFormatter DATE_FORMAT = ISODateTimeFormat.dateTime();
Once these two changes are made, the DateTime object is finally properly created... but it will be created in your local time zone, not in UTC (it will correctly adjust the time to your zone. You can easily switch this back to UTC by doing:
DateTime m = ((DateTime) (gson.fromJson(testTime, DateTimeType))).withZone(DateTimeZone.UTC);
Once you make these three changes, your tests will pass. However: I strongly advise against using JsonSerializer and JsonDeserializer, they have been deprecated in favor of TypeAdapter, whose streaming API is significantly more performant:
New applications should prefer TypeAdapter, whose streaming API is more efficient than this interface's tree API.
I am aware the user guide provides code for how to do it with the JsonSerializer / JsonDeserializer API, but that's just because they haven't yet updated it.
It would simply be something like this:
public class DateTimeAdapter extends TypeAdapter<DateTime> {
private static final DateTimeFormatter FORMAT = ISODateTimeFormat.dateTime();
public DateTime read(JsonReader reader) throws IOException {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return null;
}
String dateString = reader.nextString();
if(dateString.length() == 0) return null;
return FORMAT.parseDateTime(dateString);
}
public void write(JsonWriter writer, DateTime value) throws IOException {
if (value == null) {
writer.nullValue();
return;
}
writer.value(FORMAT.print(value));
}
}

Jackson map different attributes based on json in the same class

I have a class Response which has an attribute data.
A json file is mapped to this object. The data attribute can be of type TaskData or SubmitData on the json.
If the json has the object of type TaskData the object mapper must map to TaskData class or should map to `SubmitData' class.
In addition to the answer by Guillaume Polet, and if you can modify the JSON schema, this can also be done a little bit smoother using Jackson's Polymorphic (de)serialization via annotations:
#JsonTypeInfo(use=JsonTypeInfo.Id.Class, include=JsonTypeInfo.As.PROPERTY, property="#class")
class Data {}
class TaskData extends Data {}
class SubmitData extends Data {}
This will write out the full Java class name as an additional #class property. The json needs to include the #class property on input, however.
Instead of JsonTypeInfo.Id.Class it is also possible to perform explicit naming
#JsonTypeInfo(use=JsonTypeInfo.Id.Class, include=JsonTypeInfo.As.PROPERTY, property="#dataType")
#JsonSubTypes({
JsonSubTypes.Type(value=TaskData.class, name="task"),
JsonSubTypes.Type(value=SubmitData.class, name="submit")
})
class Data {}
#JsonTypeName("task")
class TaskData extends Data {}
#JsonTypeName("submit")
class SubmitData extends Data {}
This will yield an additional synthetic field #dataType, which will need to be present in the input.
If you cannot make the type explicit in the input JSON, you will need to stick to the manual approach.
You need to type your Response class as follows: public class Response<T>.
Then, when deserializing the input, provide a TypeReference to jackson to indicate the desired type.
See this example:
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class TestJacksonTyping {
public static void main(String[] args) throws JsonGenerationException, JsonMappingException, IOException {
ObjectMapper mapper = new ObjectMapper();
Response<TaskData> taskResponse = new Response<TaskData>();
TaskData taskData = new TaskData();
taskData.setTaskTitle("Some title");
taskResponse.setData(taskData);
Response<SubmitData> submitResponse = new Response<SubmitData>();
SubmitData submitData = new SubmitData();
submitData.setSubmitValue(256);
submitResponse.setData(submitData);
StringWriter sw = new StringWriter();
mapper.writeValue(sw, taskResponse);
String taskResponseJson = sw.toString();
mapper.writeValue(sw = new StringWriter(), submitResponse);
String submitResponseJson = sw.toString();
Response<TaskData> deserializedTaskResponse = mapper.reader(new TypeReference<Response<TaskData>>() {
}).readValue(new StringReader(taskResponseJson));
Response<SubmitData> deserializedSubmitResponse = mapper.reader(new TypeReference<Response<SubmitData>>() {
}).readValue(new StringReader(submitResponseJson));
System.out.println(deserializedTaskResponse.getData().getTaskTitle());
System.out.println(deserializedSubmitResponse.getData().getSubmitValue());
}
public static class Response<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
public static class TaskData {
private String taskTitle;
public String getTaskTitle() {
return taskTitle;
}
public void setTaskTitle(String taskTitle) {
this.taskTitle = taskTitle;
}
}
public static class SubmitData {
private int submitValue;
public int getSubmitValue() {
return submitValue;
}
public void setSubmitValue(int submitValue) {
this.submitValue = submitValue;
}
}
}

Categories