I ask here since I don't think this is a relevant question for Moshi GitHub issues.
I have card json variant #1:
{ "id":"some id",
"type":"GENERIC_MESSAGE",
"data": { "title":"Small error",
"message":"Please update some info",
"type":"ERROR"
}
}
and variant #2:
{ "id":"some id",
"type":"DATA_SCIENCE",
"data": { "cardData":
{ "message":"You spent...",
"title":"...last month"}
}
}
}
Here is my code for generic JSON adapter:
public class CardJsonAdapter
{
#Keep
static class CardJson
{
String id;
Card.Type type;
JSONObject data; <--- HERE is my problem
}
#FromJson
Card fromJson(#NonNull final CardJson json)
throws IOException
{
try
{
CardData data = getCardData(json);
return new Card(json.id, json.type, data);
}
catch (JSONException e)
{
throw new JsonDataException("Can not parse Card json");
}
}
private CardData getCardData(final #NonNull CardJson json)
throws JSONException
{
CardData data = null;
switch (json.type)
...
}
...
}
So by the card type, I already know how to parse data object. But I don't know how to get something generic in data. I can not set the type to String since Moshi crashes with BEGIN_OBJECT not expected error, I can not put Map<String, Object> it also fails with the error with the second json. And JsonObject is not crashing with parsing but is completely empty after parsing.
I can not find anything yet, so I'm asking your advice
I found the solution:
public class CardJsonAdapter
{
#Keep
static class CardDataJson
{
String title;
String message;
String type;
CardDataJson cardData;
}
#Keep
static class CardJson
{
String id;
Card.Type type;
CardDataJson data;
}
#FromJson
Card fromJson(#NonNull final CardJson json)
throws IOException
{
try
{
CardData data = getCardData(json);
return new Card(json.id, json.type, data);
}
catch (JSONException e)
{
throw new JsonDataException("Can not parse Card json");
}
}
private CardData getCardData(final #NonNull CardJson json)
throws JSONException
{
CardData data = null;
switch (json.type)
...
}
...
}
Moshi is just nulling fields that are not present in json
Related
Consider a json of type "Clothing":
{
"id":"123",
"version":2,
"apparel":{
"category":[
{
"id":"a1",
"style":"top",
"comments":[
{
"header":{
"type":"apparel.detail.Summary",
"major_version":1,
"minor_version":0
},
"summary": "notes"
}]
}
]
},
"accessories":[
{
"header":{
"type":"accessories.detail.Handbag",
"major_version":1,
"minor_version":0
},
"details":{
"brand":"Gucci",
"sno.":"G12"
},
"color":"Red",
},
{
"header":{
"type":"accessories.detail.Hat",
"major_version":1,
"minor_version":0
},
"details":{
"brand":"Adidas",
"sno.":"A12"
}
}
]
}
"Clothing" is not accessible to me and I cannot add any field level or class level json annotations.
There is a property "header" in json that helps me to determine the type of class I want to convert that entity into. I will remove the header from my json once the class type is determined (since header is not defined in my target class type because of which deserialization will fail)
I need to write a custom deserializer that returns a generic class type object. It will check if there is header, fetch target class name, remove header and deserialize it to the fetched target class and return.
This is the code that I have written, but it does not work and I am not even sure if it is possible to have a custom deserializer injected in SimpleModule with a generic return type.
#Singleton
#Provides
private Transformer provideTransformer(final HeaderDeserializer headerDeserializer) {
final SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(Object.class, headerDeserializer);
mapper.registerModule(simpleModule);
}
#Singleton
#Provides
private HeaderDeserializer provideHeaderDeserializer(final ObjectMapper objectMapper) {
return new HeaderDeserializer(objectMapper);
}
#Singleton
#Provides
private ObjectMapper provideObjectMapper() {
final ObjectMapper mapper = new ObjectMapper()
// Tell object mapper how to handle joda-time.
.registerModule(new JodaModule())
// include non-null values only
.setSerializationInclusion(Include.NON_NULL)
// ensures that timezone is preserved
.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
return mapper;
}
My HeaderDeserializer looks something like this:
public class HeaderDeserializer<T> extends StdDeserializer<T> {
private static final long serialVersionUID = 1L;
private final ObjectMapper mapper;
public HeaderDeserializer(final ObjectMapper mapper) {
this(null, mapper);
}
public HeaderDeserializer(final Class<?> vc, final ObjectMapper mapper) {
super(vc);
this.mapper = mapper;
}
#Override
public T deserialize(final JsonParser jp, final DeserializationContext ctx) {
Object value = null;
try {
JsonNode node = this.mapper.readTree(jp);
JsonNode header = node.get("header");
if (node.has("header")) {
String targetClass = header.get("type").textValue();
removeHeaderFromJsonDoc(node);
value = this.mapper.readValue(jp, Class.forName(targetClass));
}
} catch (final IOException e) {
throw new UncheckedIOException(e);
} catch (final ClassNotFoundException e) {
// do somehting
}
return (T) value;
}
private void removeHeaderFromJsonDoc(final JsonNode document) {
final Iterator<Entry<String, JsonNode>> itr = document.fields();
while (itr.hasNext()) {
final Entry<String, JsonNode> childNodeEntry = itr.next();
if (childNodeEntry.getKey().equals("header")) {
itr.remove();
}
}
}
}
And my main deserializer which will use the custom deserializer defined above looks like:
public final Clothing deserialize(
final String stringValue,
final Class<? extends Clothing> clazz) {
try {
return this.objectMapper.readValue(stringValue, clazz);
} catch (final IOException e) {
throw new IllegalArgumentException();
}
}
this.objectMapper.readValue(stringValue, clazz);
Class type of 'clazz' in this readValue method should match class type passed in simpleModule.addDeserializer.
It is not going inside your deserializer because you are adding deserializer to SimpleModule for 'Object' class and reading value for different class passed to 'Clothing deserialize',
In my Spring Boot app I have Entity like that:
#Entity
#Table(name = "UserENT")
public class User implements Serializable {
#Id
private String id;
private Data data;
...
I would like to achieve that object Data will be stored in DB in json format. But it will be mapped on Data object when selecting from DB.
Thanks for any advice.
You can implement a javax.persistence.AttributeConverter, as in:
public class DataJsonConverter implements AttributeConverter<Data, String> {
private ObjectMapper objectMapper = ...;
#Override
public String convertToDatabaseColumn(Data data) {
try {
return objectMapper.writeValueAsString(data);
} catch (JsonProcessingException e) {
throw new RuntimeException("Could not convert to Json", e);
}
}
#Override
public Data convertToEntityAttribute(String json) {
try {
return objectMapper.readValue(json, Data.class);
} catch (IOException e) {
throw new RuntimeException("Could not convert from Json", e);
}
}
}
You can then use it by annotating your field:
#Convert(converter = DataJsonConverter.class)
private Data data;
I have a json that looks like this:
[
{
_id: "54b8f62fa08c286b08449b8f",
loc: [
36.860983,
31.0567
]
},
{
_id: "54b8f6aea08c286b08449b93",
loc: {
coordinates: [ ]
}
}
]
As you can see, loc object is sometimes is a json object, sometimes is a double array. Without writing a custom deserializer, is there a way to avoid JsonSyntaxException and set the loc object to null when it is a json object rather than a double array.
There aren't any easy way (I mean a property/method call at Gson) for custom seralization/deserialization of a specific field at a json value.
You can see source code of com.google.gson.internal.bind.ReflectiveTypeAdapterFactory, and debug on its inner class Adapter's read method. (That's where your JsonSyntaxException occurs)
You can read Custom serialization for JUST specific fields and track its links. It may be implemented at future release of Gson. (Not available at latest release 2.2.4)
I would write some code for this. Maybe that's not what you are looking for but it may help somebody else.)
Solution 1 (This has less code compared with the second solution but second solution's performance is much more better):
public class SubClass extends BaseClass {
private double[] loc;
}
public class BaseClass {
#SerializedName("_id")
private String id;
}
public class CustomTypeAdapter extends TypeAdapter<BaseClass> {
private Gson gson;
public CustomTypeAdapter() {
this.gson = new Gson();
}
#Override
public void write(JsonWriter out, BaseClass value)
throws IOException {
throw new RuntimeException("Not implemented for this question!");
}
#Override
public BaseClass read(JsonReader in) throws IOException {
BaseClass instance;
try {
instance = gson.fromJson(in, SubClass.class);
} catch (Exception e) {
e.printStackTrace();
instance = gson.fromJson(in, BaseClass.class);
}
return instance;
}
}
Test:
private void test() {
String json = "[{_id:\"54b8f62fa08c286b08449b8f\",loc:[36.860983,31.0567]},{_id:\"54b8f6aea08c286b08449b93\",loc:{coordinates:[]}}]";
Type collectionType = new TypeToken<List<BaseClass>>(){}.getType();
Gson gson = new GsonBuilder().registerTypeAdapter(BaseClass.class, new CustomTypeAdapter()).create();
List<BaseClass> list = gson.fromJson(json, collectionType);
for(BaseClass item : list) {
if(item instanceof SubClass) {
System.out.println("item has loc value");
SubClass subClassInstance = (SubClass)item;
} else {
System.out.println("item has no loc value");
BaseClass baseClassInstance = item;
}
}
}
Solution 2 (It is one of the Gson Developers suggestion. See original post.):
Copy below class to your project. It is going to be a base class for your custom TypeAdapterFactorys.
public abstract class CustomizedTypeAdapterFactory<C>
implements TypeAdapterFactory {
private final Class<C> customizedClass;
public CustomizedTypeAdapterFactory(Class<C> customizedClass) {
this.customizedClass = customizedClass;
}
#SuppressWarnings("unchecked") // we use a runtime check to guarantee that 'C' and 'T' are equal
public final <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
return type.getRawType() == customizedClass
? (TypeAdapter<T>) customizeMyClassAdapter(gson, (TypeToken<C>) type)
: null;
}
private TypeAdapter<C> customizeMyClassAdapter(Gson gson, TypeToken<C> type) {
final TypeAdapter<C> delegate = gson.getDelegateAdapter(this, type);
final TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);
return new TypeAdapter<C>() {
#Override public void write(JsonWriter out, C value) throws IOException {
JsonElement tree = delegate.toJsonTree(value);
beforeWrite(value, tree);
elementAdapter.write(out, tree);
}
#Override public C read(JsonReader in) throws IOException {
JsonElement tree = elementAdapter.read(in);
afterRead(tree);
return delegate.fromJsonTree(tree);
}
};
}
/**
* Override this to muck with {#code toSerialize} before it is written to
* the outgoing JSON stream.
*/
protected void beforeWrite(C source, JsonElement toSerialize) {
}
/**
* Override this to muck with {#code deserialized} before it parsed into
* the application type.
*/
protected void afterRead(JsonElement deserialized) {
}
}
Write your POJO and your custom CustomizedTypeAdapterFactory. Override afterRead method and handle double array as you asked at your question:
public class MyClass {
#SerializedName("_id")
private String id;
private double[] loc;
// getters/setters
}
private class MyClassTypeAdapterFactory extends CustomizedTypeAdapterFactory<MyClass> {
private MyClassTypeAdapterFactory() {
super(MyClass.class);
}
#Override protected void afterRead(JsonElement deserialized) {
try {
JsonArray jsonArray = deserialized.getAsJsonObject().get("loc").getAsJsonArray();
System.out.println("loc is not a double array, its ignored!");
} catch (Exception e) {
deserialized.getAsJsonObject().remove("loc");
}
}
}
Test:
private void test() {
String json = "[{_id:\"54b8f62fa08c286b08449b8f\",loc:[36.860983,31.0567]},{_id:\"54b8f6aea08c286b08449b93\",loc:{coordinates:[]}}]";
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new MyClassTypeAdapterFactory())
.create();
Type collectionType = new TypeToken<List<MyClass>>(){}.getType();
List<MyClass> list = gson.fromJson(json, collectionType);
for(MyClass item : list) {
if(item.getLoc() != null) {
System.out.println("item has loc value");
} else {
System.out.println("item has no loc value");
}
}
}
This is how I did this. It is shorter, but I think #DevrimTuncers answer is the best one.
//This is just Double array to use as location object
public class Location extends ArrayList<Double> {
public Double getLatidute() {
if (this.size() > 0) {
return this.get(0);
} else {
return (double) 0;
}
}
public Double getLongitude() {
if (this.size() > 1) {
return this.get(1);
} else {
return (double) 0;
}
}
public static class LocationDeserializer implements JsonDeserializer<Location> {
#Override
public Location deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
try {
JsonArray array = json.getAsJsonArray();
Location location = new Location();
for (int i = 0; i < array.size(); i++) {
location.add(array.get(i).getAsDouble());
}
return location;
} catch (Exception e) {
return null;
}
}
}
}
I have a Java class in a servlet that uses GSON to render posted JSON Strings into a Java object. The beauty of the approach is, that GSON filters out all JSON elements that don't match a class property, so I never end up with JSON content that I don't want to process. The servlet's doPost (simplified) looks like this:
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
PrintWriter out = null;
try {
InputStream in = request.getInputStream();
Demo d = Demo.load(in);
in.close();
response.setContentType("text/plain");
response.setStatus(HttpServletResponse.SC_OK);
out = response.getWriter();
out.println(d.toJson);
} catch (Exception e) {
e.printStackTrace();
out.println(e.getMessage());
}
out.close();
}
The Demo class (and that's the one I need to recreate in common.js or node.js looks like this:
#JsonIgnoreProperties(ignoreUnknown = true)
public class Demo implements Serializable {
public static Demo load(InputStream in) {
Demo result = null;
try {
Gson gson = new GsonBuilder().create();
result = gson.fromJson(new InputStreamReader(in), Demo.class);
} catch (Exception e) {
result = null;
}
return result;
}
#TypeDiscriminator
#JsonProperty("_id")
private String id = UUID.randomUUID().toString();
private Date created = new Date();
private String color;
private String name;
private String taste;
public String getColor() {
return this.color;
}
public String getName() {
return this.name;
}
public String getTaste() {
return this.taste;
}
public Date getCreated() {
return this.created;
}
public String getId() {
return this.id;
}
public void setName(String name) {
this.name = name;
}
public void setTaste(String taste) {
this.taste = taste;
}
public void setColor(String color) {
this.color = color;
}
public String toJson() {
GsonBuilder gb = new GsonBuilder();
gb.setPrettyPrinting();
gb.disableHtmlEscaping();
Gson gson = gb.create();
return gson.toJson(this);
}
}
Obviously I stripped out all the processing logic and the servlet just echos the JSON back, which is not what the app does, but serves to illustrate the point. I can throw pretty any String in a HTTP Post at that example and I only get valid Demo objects.
How would I do something like this in node.js?
Node.js is Javascript so has built in support for json. You can use JSON.parse to convert from string to json and wrap in try catch block.
To only include select properties there is no built in feature in node that I know of unless you are using Mongodb with mongoose, but you could do following: Have a "class" that is an object containing all properties that you want and delete those from parsed json object that are not in that "class" object.
var class = {x: null, y:null};
for(var prop in object){
if (!class.hasOwnProperty (prop)) {
delete object [prop]
}
It would be best to use this class as object and expose parseJSON function to encapsulate this functionality
I'm working with an api (Phillips Hue) that wraps all of it's json responses in an array with one entry (the content).
Example:
[{
"error": {
"type": 5,
"address": "/",
"description": "invalid/missing parameters in body"
}
}]
I usually write standard POJO's parsed by GSON to handle responses but since the response is not a json object I'm a bit stumped on the best way to deal with this. I didn't really want every object to actually be an array that I have to call .get(0) on.
Example of the POJO if it was a JSON obj and NOT wrapped in an array.
public class DeviceUserResponse {
private DeviceUser success;
private Error error;
public DeviceUser getSuccess() {
return success;
}
public Error getError() {
return error;
}
public static class Error {
private int type;
private String address;
private String description;
public int getType() {
return type;
}
public String getAddress() {
return address;
}
public String getDescription() {
return description;
}
#Override
public String toString() {
return "Type: " + this.type
+ " Address: " + this.address
+ " Description: " + this.description;
}
}
}
What I have to do right now:
ArrayList<DeviceUserResponse> response.get(0).getError();
Is there a way that I can strip this array for every response or am I just going to have to do a .get(0) in my POJO's and just not expose it?
I think you've to go with custom deserialization in order to "strip out" the array.
Here a possible solution.
An adapter for your response POJO:
public class DeviceUserResponseAdapter extends TypeAdapter<DeviceUserResponse> {
protected TypeAdapter<DeviceUserResponse> defaultAdapter;
public DeviceUserResponseAdapter(TypeAdapter<DeviceUserResponse> defaultAdapter) {
this.defaultAdapter = defaultAdapter;
}
#Override
public void write(JsonWriter out, DeviceUserResponse value) throws IOException {
defaultAdapter.write(out, value);
}
#Override
public DeviceUserResponse read(JsonReader in) throws IOException {
in.beginArray();
assert(in.hasNext());
DeviceUserResponse response = defaultAdapter.read(in);
in.endArray();
return response;
}
}
A factory for your adapter:
public class DeviceUserResponseAdapterFactory implements TypeAdapterFactory {
#Override
#SuppressWarnings("unchecked")
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
if (type.getRawType()!=DeviceUserResponse.class) return null;
TypeAdapter<DeviceUserResponse> defaultAdapter = (TypeAdapter<DeviceUserResponse>) gson.getDelegateAdapter(this, type);
return (TypeAdapter<T>) new DeviceUserResponseAdapter(defaultAdapter);
}
}
Then you've to register and user it:
DeviceUserResponseAdapterFactory adapterFactory = new DeviceUserResponseAdapterFactory();
GsonBuilder gsonBuilder = new GsonBuilder();
Gson gson = gsonBuilder.registerTypeAdapterFactory(adapterFactory).create();
DeviceUserResponse response = gson.fromJson(json, DeviceUserResponse.class);
System.out.println(response.getError());
This solution will not work if you have the DeviceUserResponse inside other complex JSON object. I that case the adapter will try to find an array and will terminate with an error.
Another solution is to parse it as array and then in your "communication" layer you get only the first element. This will preserve the GSon deserialization.
In the comment you're asking for a more generic solution, here one:
The adapter:
public class ResponseAdapter<T> extends TypeAdapter<T> {
protected TypeAdapter<T> defaultAdapter;
public ResponseAdapter(TypeAdapter<T> defaultAdapter) {
this.defaultAdapter = defaultAdapter;
}
#Override
public void write(JsonWriter out, T value) throws IOException {
defaultAdapter.write(out, value);
}
#Override
public T read(JsonReader in) throws IOException {
in.beginArray();
assert(in.hasNext());
T response = defaultAdapter.read(in);
in.endArray();
return response;
}
}
The factory:
public class ResponseAdapterFactory implements TypeAdapterFactory {
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
if ((type.getRawType().getSuperclass() != Response.class)) return null;
TypeAdapter<T> defaultAdapter = (TypeAdapter<T>) gson.getDelegateAdapter(this, type);
return (TypeAdapter<T>) new ResponseAdapter<T>(defaultAdapter);
}
}
Where Response.class is your super class from which all the service responses inherit.
The first solution advices are still valid.