Dearest Stackoverflowers,
I was wondering if anyone knows how to solve this the best way;
I'm talking to an api which returns a json object like this:
{
"field1": "value1",
"field2": "value2",
"details": {
"nested1": 1,
"nested2": 1
}
In java I have an object (entity) which for example, would have all these fields, but with the details as loose fields, so:
field1, field2, nested1, nested2.
This because It's an android project and I can't just go saving a class with info into my entity since I'm bound to ormlite.
Is there any way to convert the fields flat into my object using GSON? note that I'm using a generic class to convert these right now straight from the API. And I want to store these fields (which contain information as an int). In the same entity.
You can write a custom type adapter to map json value to your pojo.
Define a pojo:
public class DataHolder {
public List<String> fieldList;
public List<Integer> detailList;
}
Write a custom typeAdapter:
public class CustomTypeAdapter extends TypeAdapter<DataHolder> {
public DataHolder read(JsonReader in) throws IOException {
final DataHolder dataHolder = new DataHolder();
in.beginObject();
while (in.hasNext()) {
String name = in.nextName();
if (name.startsWith("field")) {
if (dataHolder.fieldList == null) {
dataHolder.fieldList = new ArrayList<String>();
}
dataHolder.fieldList.add(in.nextString());
} else if (name.equals("details")) {
in.beginObject();
dataHolder.detailList = new ArrayList<Integer>();
} else if (name.startsWith("nested")) {
dataHolder.detailList.add(in.nextInt());
}
}
if(dataHolder.detailList != null) {
in.endObject();
}
in.endObject();
return dataHolder;
}
public void write(JsonWriter writer, DataHolder value) throws IOException {
throw new RuntimeException("CustomTypeAdapter's write method not implemented!");
}
}
Test:
String json = "{\"field1\":\"value1\",\"field2\":\"value2\",\"details\":{\"nested1\":1,\"nested2\":1}}";
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(DataHolder.class, new CustomTypeAdapter());
Gson gson = builder.create();
DataHolder dataHolder = gson.fromJson(json, DataHolder.class);
Output:
About TypeAdapter:
https://google-gson.googlecode.com/svn/trunk/gson/docs/javadocs/com/google/gson/TypeAdapter.html
http://www.javacreed.com/gson-typeadapter-example/
Related
In a request to the Reddit API, there is a field associated with each post called edited. This field either has a boolean value or if a post has been edited, has a long value which I think is the timestamp of when the post was edited. How can I deserialize this with GSON without knowing the type? If I try to deserialize into a Boolean value I get an exception if a timestamp is present.
Image below of JSON response:
Annotating the dynamic field with #JsonAdapter is probably the easiest way to work around this (assuming you have a DTO class):
final class Datum {
#JsonAdapter(MaybeLongTypeAdapter.class)
final Long edited = null;
}
Where MaybeLongTypeAdapter is as follows:
final class MaybeLongTypeAdapter
extends TypeAdapter<Long> {
private MaybeLongTypeAdapter() {
}
#Override
public void write(final JsonWriter out, final Long value) {
throw new UnsupportedOperationException();
}
#Override
public Long read(final JsonReader in)
throws IOException {
switch ( in.peek() ) {
case NULL:
return null;
case BOOLEAN:
if ( in.nextBoolean() ) {
throw new JsonSyntaxException("Unexpected `true` at " + in);
}
return null;
case NUMBER:
return in.nextLong();
default:
throw new JsonSyntaxException("Unexpected element at " + in);
}
}
}
The type adapter above is pretty self-descriptive. It can be implemented in a more generic way, of course, but it's out of scope here. Additionally, please note that it does not pick the original Long type adapter that can be re-configured in GsonBuilder. Example of use:
private static final Gson gson = new Gson();
private static final Type listOfDatumType = new TypeToken<List<Datum>>() {}.getType();
public static void main(final String... args) {
final String json = "[{\"edited\": false},{\"edited\": 1527130582}]";
final List<Datum> data = gson.fromJson(json, listOfDatumType);
for ( final Datum datum : data ) {
System.out.println(datum.edited);
}
}
Output:
null
1527130582
I'm willing to try the new Room Library from Android and I met the below error:
Error:(19, 29) error: Cannot figure out how to save this field into
database. You can consider adding a type converter for it.
This error refers to the following class member:
private HashSet<String> fruits;
I have the following class:
#Entity(tableName = "SchoolLunches")
public class SchoolLunch {
#PrimaryKey(autoGenerate = true)
private int lunchId;
private boolean isFresh;
private boolean containsMeat;
private HashSet<String> fruits;
public int getLunchId() {
return lunchId;
}
public void setLunchId(int lunchId) {
this.lunchId = lunchId;
}
public boolean isFresh() {
return isFresh;
}
public void setFresh(boolean fresh) {
isFresh = fresh;
}
public boolean isContainsMeat() {
return containsMeat;
}
public void setContainsMeat(boolean containsMeat) {
this.containsMeat = containsMeat;
}
public HashSet<String> getFruits() {
return fruits;
}
public void setFruits(HashSet<String> fruits) {
this.fruits = fruits;
}
Also, there is a relative DAO class:
#Dao
public interface SchoolLunchDAO {
#Query("SELECT * FROM SchoolLunches")
List<SchoolLunch> getAll();
#Insert
void insertAll(SchoolLunch... schoolLunches);
#Query("DELETE FROM SchoolLunches")
void deleteAll();
}
Since I'm trying to be a very good developer, I wrote a unit test as follows:
#Test
public void singleEntityTest() {
HashSet<String> fruitSet = new HashSet<>();
fruitSet.add("Apple");
fruitSet.add("Orange");
SchoolLunch schoolLunch = new SchoolLunch();
schoolLunch.setContainsMeat(false);
schoolLunch.setFresh(true);
schoolLunch.setFruits(fruitSet);
schoolLunchDAO.insertAll(schoolLunch);
List<SchoolLunch> schoolLunches = schoolLunchDAO.getAll();
assertEquals(schoolLunches.size(), 1);
SchoolLunch extractedSchoolLunch = schoolLunches.get(0);
assertEquals(false, extractedSchoolLunch.isContainsMeat());
assertEquals(true, extractedSchoolLunch.isFresh());
assertEquals(2, extractedSchoolLunch.getFruits().size());
}
What should I do here?
What should I do here?
You could create a type converter, as suggested by the error message. Room does not know how to persist a HashSet<String>, or a Restaurant, or other arbitrary objects.
Step #1: Decide what basic type you want to convert your HashSet<String> into (e.g., a String)
Step #2: Write a class with public static type conversion methods, annotated with #TypeConverter, to do the conversion (e.g., HashSet<String> to String, String to HashSet<String>), in some safe fashion (e.g., use Gson, formatting your String as JSON)
Step #3: Add a #TypeConverters annotation to your RoomDatabase or other scope, to teach Room about your #TypeConverter methods
For example, here are a pair of type converter methods for converting a Set<String> to/from a regular String, using JSON as the format of the String.
#TypeConverter
public static String fromStringSet(Set<String> strings) {
if (strings==null) {
return(null);
}
StringWriter result=new StringWriter();
JsonWriter json=new JsonWriter(result);
try {
json.beginArray();
for (String s : strings) {
json.value(s);
}
json.endArray();
json.close();
}
catch (IOException e) {
Log.e(TAG, "Exception creating JSON", e);
}
return(result.toString());
}
#TypeConverter
public static Set<String> toStringSet(String strings) {
if (strings==null) {
return(null);
}
StringReader reader=new StringReader(strings);
JsonReader json=new JsonReader(reader);
HashSet<String> result=new HashSet<>();
try {
json.beginArray();
while (json.hasNext()) {
result.add(json.nextString());
}
json.endArray();
}
catch (IOException e) {
Log.e(TAG, "Exception parsing JSON", e);
}
return(result);
}
I created the following class and now it works. Thank you, CommonsWare!
public class Converters {
private static final String SEPARATOR = ",";
#TypeConverter
public static HashSet<String> fromString(String valueAsString) {
HashSet<String> hashSet = new HashSet<>();
if (valueAsString != null && !valueAsString.isEmpty()) {
String[] values = valueAsString.split(SEPARATOR);
hashSet.addAll(Arrays.asList(values));
}
return hashSet;
}
#TypeConverter
public static String hashSetToString(HashSet<String> hashSet) {
StringBuilder stringBuilder = new StringBuilder();
for (String currentElement : hashSet) {
stringBuilder.append(currentElement);
stringBuilder.append(SEPARATOR);
}
return stringBuilder.toString();
}
}
Here is the json schema:
As you can see, rated can be both boolean and object.
I am using Retrofit 2 and Gson converter. How should I create my model for this schema?
Here's how I solved this issue:
Create a custom type adapter in your model and parse rated manually;
public class AccountState {
//#SerializedName("rated") //NOPE, parse it manually
private Integer mRated; //also don't name it rated
public Integer getRated() {
return mRated;
}
public void setRated(Integer rated) {
this.mRated = rated;
}
public static class AccountStateDeserializer implements JsonDeserializer<AccountState> {
#Override
public AccountState deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
AccountState accountState = new Gson().fromJson(json, AccountState.class);
JsonObject jsonObject = json.getAsJsonObject();
if (jsonObject.has("rated")) {
JsonElement elem = jsonObject.get("rated");
if (elem != null && !elem.isJsonNull()) {
if(elem.isJsonPrimitive()){
accountState.setRated(null);
}else{
accountState.setRated(elem.getAsJsonObject().get("value").getAsInt());
}
}
}
return accountState ;
}
}
}
Here you create your gson with this custom adapter:
final static Gson gson = new GsonBuilder()
.registerTypeAdapter(AccountState.class, new AccountState.AccountStateDeserializer())
.create();
Add it to retrofit like that:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BuildConfig.ENDPOINT)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient)
.build();
TADADADADADADADDAD!
You can make it work without having to implement a custom converter.
All you have to do is put a general "Object" type for the variable and then you just check which data type it is by doing this:
if(object.getClass == YourClass.class){
Whatever we = ((YourClass) object).getWhatever();
} else if(object.getClass == YourOtherClass.class){
String name = ((YourOtherClass) object).getName();
}
You can add as many data types to this variable as you like.
You can also use the java types "String.class", "Boolean.class" or whatever you like.
Gson has a nice feature allowing to inject a custom type adapter or a type adapter factory to a certain field therefore letting Gson to manage the host object and the latter's fields (de)serialization. So, you can be sure that AccountState could be still deserialized with ReflectiveTypeAdapterFactory and ReflectiveTypeAdapterFactory.Adapter so all deserialization strategies defined in GsonBuilder could be applied.
final class AccountState {
// This is what can make life easier. Note its advantages:
// * PackedBooleanTypeAdapterFactory can be reused multiple times
// * AccountState life-cycle can be managed by Gson itself,
// so it can manage *very* complex deserialization automatically.
#JsonAdapter(PackedBooleanTypeAdapterFactory.class)
final Boolean rated = null;
}
Next, how PackageBooleanTypeAdapterFactory is implemented:
final class PackedBooleanTypeAdapterFactory
implements TypeAdapterFactory {
// Gson can instantiate this itself, no need to expose
private PackedBooleanTypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// Check if it's the type we can handle ourself
if ( typeToken.getRawType() == Boolean.class ) {
final TypeAdapter<Boolean> typeAdapter = new PackedIntegerTypeAdapter(gson);
// Some Java "unchecked" boilerplate here...
#SuppressWarnings("unchecked")
final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) typeAdapter;
return castTypeAdapter;
}
// If it's something else, let Gson pick a downstream type adapter on its own
return null;
}
private static final class PackedIntegerTypeAdapter
extends TypeAdapter<Boolean> {
private final Gson gson;
private PackedIntegerTypeAdapter(final Gson gson) {
this.gson = gson;
}
#Override
public void write(final JsonWriter out, final Boolean value) {
throw new UnsupportedOperationException();
}
#Override
public Boolean read(final JsonReader in)
throws MalformedJsonException {
// Pick next token as a JsonElement
final JsonElement jsonElement = gson.fromJson(in, JsonElement.class);
// Note that Gson uses JsonNull singleton to denote a null
if ( jsonElement.isJsonNull() ) {
return null;
}
if ( jsonElement.isJsonPrimitive() ) {
return jsonElement
.getAsJsonPrimitive()
.getAsBoolean();
}
if ( jsonElement.isJsonObject() ) {
return jsonElement
.getAsJsonObject()
.getAsJsonPrimitive("value")
.getAsBoolean();
}
// Not something we can handle
throw new MalformedJsonException("Cannot parse: " + jsonElement);
}
}
}
Demo:
public static void main(final String... args) {
parseAndDump("{\"rated\":null}");
parseAndDump("{\"rated\":true}");
parseAndDump("{\"rated\":{\"value\":true}}");
}
private static void parseAndDump(final String json) {
final AccountState accountState = gson.fromJson(json, AccountState.class);
System.out.println(accountState.rated);
}
Output:
null
true
true
Note that JsonSerializer and JsonDeserializer both have some performance and memory cost due to its tree model design (you can traverse JSON trees easily as long as they are in memory). Sometimes, for simple cases, a streaming type adapter may be preferable. Pros: consumes less memory and works faster. Cons: hard to implement.
final class AccountState {
#JsonAdapter(PackedBooleanTypeAdapter.class)
final Boolean rated = null;
}
Note that the rated field accepts a type adapter directly because it does not need Gson instances to build JSON trees (JsonElements).
final class PackedBooleanTypeAdapter
extends TypeAdapter<Boolean> {
// Gson still can instantiate this type adapter itself
private PackedBooleanTypeAdapter() {
}
#Override
public void write(final JsonWriter out, final Boolean value) {
throw new UnsupportedOperationException();
}
#Override
public Boolean read(final JsonReader in)
throws IOException {
// Peeking the next JSON token and dispatching parsing according to the given token
final JsonToken token = in.peek();
switch ( token ) {
case NULL:
return parseAsNull(in);
case BOOLEAN:
return parseAsBoolean(in);
case BEGIN_OBJECT:
return parseAsObject(in);
// The below might be omitted, since some code styles prefer all switch/enum constants explicitly
case BEGIN_ARRAY:
case END_ARRAY:
case END_OBJECT:
case NAME:
case STRING:
case NUMBER:
case END_DOCUMENT:
throw new MalformedJsonException("Cannot parse: " + token);
// Not a known token, and must never happen -- something new in a newer Gson version?
default:
throw new AssertionError(token);
}
}
private Boolean parseAsNull(final JsonReader in)
throws IOException {
// null token still has to be consumed from the reader
in.nextNull();
return null;
}
private Boolean parseAsBoolean(final JsonReader in)
throws IOException {
// Consume a boolean value from the reader
return in.nextBoolean();
}
private Boolean parseAsObject(final JsonReader in)
throws IOException {
// Consume the begin object token `{`
in.beginObject();
// Get the next property name
final String property = in.nextName();
// Not a value? Then probably it's not what we're expecting for
if ( !property.equals("value") ) {
throw new MalformedJsonException("Unexpected property: " + property);
}
// Assuming the property "value" value must be a boolean
final boolean value = in.nextBoolean();
// Consume the object end token `}`
in.endObject();
return value;
}
}
This one should work faster. The output remains the same. Note that Gson does not require a GsonBuilder for both cases. As far as I remember how Retrofit 2 works, GsonConverterFactory is still required (not sure, Gson is not the default serializer in Retrofit 2?).
I am trying to deserialize this JSON array into my android project.
[{"Name":"Ban","Price":1},{"Name":"Banana","Price":1},{"Name":"chicken","Price":14},{"Name":"pizza","Price":16},{"Name":"slice","Price":1}]
I have made this webservice in Asp.net.
The code I am using to deserialize it is below
public void onClick(View v)
{
String url="http://192.168.15.2/MyAndroid/InputCaller.aspx"; //do not use localhost
String response=callWebService(url);
List<Items> mObjectList = new ArrayList<Items>() ;
ItemsList list = null;
Gson gson = new Gson();
list = gson.fromJson(response, ItemsList.class);
// list = getItemsList(response);
Intent myIntent = new Intent(v.getContext(), Cart.class);
startActivity(myIntent);
}
public final ItemsList getItemsList (String jsonString)
{
ItemsList il = null;
Gson gson = new Gson();
il = gson.fromJson(jsonString, ItemsList.class);
return il;
}
public class ItemsList
{
private List<ItemsContainer> items = new ArrayList<ItemsContainer>();
public List<ItemsContainer> getItemsContainerList()
{
return items;
}
}
class ItemsContainer
{
Items items;
public Items getItem()
{
return items;
}
}
public class Items
{
String Name;
int Price;
}
It is not working and when I try to debug it I get this message on list = gson.fromJson(response, ItemsList.class);
Gson.class Source not found.
This is my first deserialisation program and I would really appreciate if anybody help me with it. Thank you,
Don't make things complicated by using further parent classes (as container) for Items class. Simply de-serialize all the items into a List object using Gson as below:
List<Items> listItems = (List<Items>) gson.fromJson(response,
new TypeToken<List<Items>>(){}.getType());
now you've got all the Items in a List object: listItems
I'm trying this code, but not working
public class Main {
public static void main(String[] args) { list();}
private static void list() {
Gson gson = new Gson();
String result = "[{\"Person\":{\"id\":\"1\",\"name\":\"Prédio I\"}},{\"Person\":{\"id\":\"2\",\"name\":\"Prédio II\"}}]";
Person[] persons = gson.fromJson(result, Person[].class);
System.out.println("Qtde: " + persons.length);
for (Person pe : persons) {
System.out.println("Name: " + pe.getName());
}
}
}
see that this code takes an array of Persons, but not works, should show me javabens objects
I'm guessing that the problem is with properly binding the JSON structure to a matching Java data structure. Note that each component in the JSON array is an object that has a single property, named "Person", which is an object with two properties, named "id" and "name" -- each component in the JSON array is not a simple Person object, but a Person wrapper object.
The following demonstrates binding the JSON to a matching Java data structure.
import com.google.gson.Gson;
public class GsonFoo
{
public static void main(String[] args)
{
// [{"Person":{"id":"1","name":"Prédio I"}},{"Person":{"id":"2","name":"Prédio II"}}]
String jsonInput = "[{\"Person\":{\"id\":\"1\",\"name\":\"Prédio I\"}},{\"Person\":{\"id\":\"2\",\"name\":\"Prédio II\"}}]";
PersonWrapper[] persons = new Gson().fromJson(jsonInput, PersonWrapper[].class);
System.out.println("Qtde: " + persons.length);
for (PersonWrapper pw : persons)
{
System.out.println("Name: " + pw.Person.name);
}
// output:
// Qtde: 2
// Name: Prédio I
// Name: Prédio II
}
}
class PersonWrapper
{
Person Person;
}
class Person
{
int id;
String name;
}