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();
}
}
I am using GSON in my android app. Currently, I am setting a method that contains a value from my JSON string called "followed". One of items in the JSON string contains followed and the second string does not. I use Realm to persist the unique object so as you can see it just gets overwritten.
Here are 2 json strings to compare as an example:
{"customer_case":"OFFICE001","circle_id":"3","address":"10 Canal St","city":"Bristol","state":"PA","zip":"19007","county":"Bucks County","apt_no":"","latitude":"40.1012666","longitude":"-74.855304","profile_picture":"uploads/thumbnails/2014/06/07/16/1402165202_3_16_539356ad9134b3.jpg","id":"539356ad9134b3","google_address":"10 Canal Street","google_city":"Bristol","google_state":"Pennsylvania","verified_zip":"19007","google_county":"Bucks County","status":"Active","add_date":"2014-06-07","circle_name":"Test Portfolio","step":"Rental","loan":"","winterized":null,"boiler":null,"sump_pump":null,"septic":null,"police_id":null,"police":null,"police_phone":null,"electric_id":null,"electric":null,"electric_phone":null,"sewer_id":null,"sewer":null,"sewer_phone":null,"water_id":null,"water":null,"water_phone":null,"fsm_company_id":"5","fsm_company":"Assero Services LLC - FSM","fsm_email":"leemertins#assero24.com","fsm_phone":"2155868317","hoa_id":null,"hoa":null,"hoa_email":null,"hoa_phone":null,"client_id":"9","client":"Test Client","client_email":"krishna162#gmail.com","client_phone":"2157830782","broker_contact_id":null,"broker":null,"broker_email":null,"broker_phone":null,"lawn_contractor":null,"cleaning_contractor":null,"bedroom":null,"bathroom":null,"sqft":null,"lot_size":null,"list_price":"538525","built":null,"assign_date":"06/07/2014","lock_box":null,"gate_code":null,"key_code":null,"property_type":"Unknown","description":null,"sub_status":null,"occupancy_status":null,"street_view":"uploads/2015/06/25/4036/0470e4cd-ce9d-4439-8031-6be5101cd09c.JPG","marketing_front":"uploads/2015/06/25/4036/b099a190-f354-454a-8479-bec67bc41988.JPG","followed":"1"}
{"customer_case":"OFFICE001","circle_id":"3","address":"10 Canal St","city":"Bristol","state":"PA","zip":"19007","county":"Bucks County","apt_no":"","latitude":"40.1012666","longitude":"-74.855304","profile_picture":"uploads/thumbnails/2014/06/07/16/1402165202_3_16_539356ad9134b3.jpg","id":"539356ad9134b3","google_address":"10 Canal Street","google_city":"Bristol","google_state":"Pennsylvania","verified_zip":"19007","google_county":"Bucks County","status":"Active","add_date":"2014-06-07","circle_name":"Test Portfolio","step":"Rental","loan":"","winterized":null,"boiler":null,"sump_pump":null,"septic":null,"police_id":null,"police":null,"police_phone":null,"electric_id":null,"electric":null,"electric_phone":null,"sewer_id":null,"sewer":null,"sewer_phone":null,"water_id":null,"water":null,"water_phone":null,"fsm_company_id":"5","fsm_company":"Assero Services LLC - FSM","fsm_email":"leemertins#assero24.com","fsm_phone":"2155868317","hoa_id":null,"hoa":null,"hoa_email":null,"hoa_phone":null,"client_id":"9","client":"Test Client","client_email":"krishna162#gmail.com","client_phone":"2157830782","broker_contact_id":null,"broker":null,"broker_email":null,"broker_phone":null,"lawn_contractor":null,"cleaning_contractor":null,"bedroom":null,"bathroom":null,"sqft":null,"lot_size":null,"list_price":"538525","built":null,"assign_date":"06/07/2014","lock_box":null,"gate_code":null,"key_code":null,"property_type":"Unknown","description":null,"sub_status":null,"occupancy_status":null,"street_view":"uploads/2015/06/25/4036/0470e4cd-ce9d-4439-8031-6be5101cd09c.JPG","marketing_front":"uploads/2015/06/25/4036/b099a190-f354-454a-8479-bec67bc41988.JPG"}
Note the difference is the followed item at the end of the json string.
From the GSON documentation it says:
"While deserialization, a missing entry in JSON results in setting the corresponding field in the object to null."
Is there a way to override this and not automatically set it to null, instead just skip the field?
Here is some code that I am using to deserialize my json:
PropertyObject prop = visnetawrap.gsonClient.fromJson(properties.get(i).toString(), PropertyObject.class);
visnetawrap.gsonClient = new GsonBuilder()
.setExclusionStrategies(new ExclusionStrategy() {
#Override
public boolean shouldSkipField(FieldAttributes f) {
return f.getDeclaringClass().equals(RealmObject.class) || f.getDeclaredClass().equals(Drawable.class);
}
#Override
public boolean shouldSkipClass(Class<?> clazz) {
return false;
}
})
.registerTypeAdapter(Date.class, new GsonDateDeserializer())
.registerTypeAdapter(Double.class, new TypeAdapter<Double>() {
#Override
public void write(JsonWriter out, Double value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
out.value(value);
}
#Override
public Double read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
String stringValue = in.nextString();
try {
return Double.valueOf(stringValue);
} catch (NumberFormatException e) {
return null;
}
}
})
.create();
Here is what I am doing as a work around:
.registerTypeAdapter(PropertyObject.class, new JsonDeserializer<PropertyObject>() {
#Override
public PropertyObject deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
PropertyObject prop = new PropertyObject();
JsonObject propObj = json.getAsJsonObject();
if (propObj.get("id") == null) {
return null;
}
prop.setPropertyId(propObj.get("id").getAsString());
if (propObj.get("followed") == null) {
Realm realmThread = Realm.getDefaultInstance();
PropertyObject existingProp = realmThread.where(PropertyObject.class).equalTo("propertyId", propObj.get("id").getAsString()).findFirst();
if (existingProp == null) {
prop.setPropertyFollowed(0);
}
else {
prop.setPropertyFollowed(existingProp.getPropertyFollowed());
}
realmThread.close();
}
else {
prop.setPropertyFollowed(propObj.get("followed").getAsInt());
}
return prop;
}
})
you may create your own TypeAdapter
public class YourTypeAdapter extends TypeAdapter<PropertyObject> {
#Override
public PropertyObject read(final JsonReader in) throws IOException {
final PropertyObject obj = new PropertyObject(); //I don't know how is your obj
in.beginObject();
boolean hasFollowedField = false;
while (in.hasNext()) {
switch (in.nextName()) {
case "gate_code":
//set value to your obj
obj.setValue(in.nextString())
break;
//do same thing to others...
//...
case "followed":
hasFollowedField = true;
//set value to obj
break;
}
if (!hasFollowedField) {
//set followed value to obj what you want
}
}
in.endObject();
return obj;
}
#Override
public void write(final JsonWriter out, final PropertyObject obj) throws IOException {
out.beginObject();
out.name("gate_code").value(gate_code.getGateCode());
//simple set name and value from obj to JsonWriter
out.endObject();
}
}
and then register the TypeAdapter to your GsonBuilder obj
hope it would help
Do custom persisters work on Android? I was trying to write one for an entity, and was having no luck in having it run when the entity gets written by the DAO. So, I tried to use the "MyDatePersister" from the examples and I am not able to get that working either.
The persister is nearly identical to the example one -> https://github.com/j256/ormlite-jdbc/blob/master/src/test/java/com/j256/ormlite/examples/datapersister/MyDatePersister.java
In my entity, I have
#DatabaseTable
public class ClickCount implements Serializable {
// other declarations
#DatabaseField(columnName = DATE_FIELD_NAME, persisterClass = MyDatePersister.class)
private Date lastClickDate;
// more code
}
Here is a link to the whole project in Bitbucket -> https://bitbucket.org/adstro/android-sandbox. It's basically one of the ORMLite Android examples with the custom persister example added.
Any feedback would be greatly appreciated.
Thanks
First off, what is the error result you're getting?
I got my custom persister to work just fine, though I didn't try to extend the DateType. Below is a JSONArrayPersister I found the need for. The confusing part is in the naming of the methods, but once they're setup properly, it should be ok.
package com.example.acme.persister;
import com.j256.ormlite.field.FieldType;
import com.j256.ormlite.field.SqlType;
import com.j256.ormlite.field.types.BaseDataType;
import com.j256.ormlite.support.DatabaseResults;
import org.json.JSONArray;
import org.json.JSONException;
import java.sql.SQLException;
public class JSONArrayPersister extends BaseDataType {
public static int DEFAULT_WIDTH = 1024;
private static final JSONArrayPersister singleTon = new JSONArrayPersister();
public static JSONArrayPersister getSingleton() {
return singleTon;
}
private JSONArrayPersister() {
super(SqlType.STRING, new Class<?>[] { String.class });
}
protected JSONArrayPersister(SqlType sqlType, Class<?>[] classes) {
super(sqlType, classes);
}
#Override
public Object parseDefaultString(FieldType fieldType, String defaultStr) {
try {
return new JSONArray(defaultStr);
} catch (JSONException ex)
{
return new JSONArray();
}
}
#Override
public Object resultToSqlArg(FieldType fieldType, DatabaseResults results, int columnPos) throws SQLException {
try {
return new JSONArray( results.getString(columnPos) );
} catch (JSONException ex)
{
return new JSONArray();
}
}
#Override
public Object resultStringToJava(FieldType fieldType, String stringValue, int columnPos) throws SQLException {
return parseDefaultString(fieldType, stringValue);
}
#Override
public int getDefaultWidth() {
return DEFAULT_WIDTH;
}
}
Then in your entity:
#DatabaseField(persisterClass = JSONArrayPersister.class)
private JSONArray something;
I have these to classes:
public class Station {
#DatabaseField(foreign = true, foreignAutoCreate = true)
private OpeningTimes openingTimes;
}
public class OpeningTimes {
#DatabaseField(generatedId = true)
int _id;
}
Now OpeningTimes row is auto created, when I call createOrUpdate method on StationDao. That's great!
I would be also thankful, if I could delete Station object and its nested object OpeningTimes automatically.
Now I have to do it this way in Station class and it seems quite confusing. Is there any more elegant way?
public void deleteFromDb(DatabaseHelper dbHelper) {
try {
openingTimes.deleteFromDb(dbHelper);
dbHelper.getStationDao().delete(this);
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
EDIT:
I have been trying also this, but with SQL Statement errors
#DatabaseField(foreign = true, foreignAutoCreate = true, columnDefinition="INTEGER, FOREIGN KEY(`openingTimes_id`) REFERENCES openingtimes(`_id`)")
I would consider doing this at the DAO level instead of at the persisted object level. What I recommend is creating your own StationDao interface and your own StationDaoImpl implementation. The ORMLite docs an example of this.
public interface StationDao extends Dao<Station, Integer> {
// we will just be overriding some of the delete methods
}
Then create your implementation which would override the delete() method and delete any children objects. Something like the following:
public class StationDaoImpl extends BaseDaoImpl<Station, Integer>
implements StationDao {
private final Dao<OpeningTimes, Integer> openTimesDao;
public AccountDaoImpl(ConnectionSource connectionSource) throws SQLException {
super(connectionSource, Station.class);
openTimesDao = DaoManager.createDao(connectionSource, OpeningTimes.class);
}
#Override
public int delete(Station station) throws SQLException {
if (station.openTimes != null) {
openTimesDao.delete(station.openTimes);
}
return super.delete(station);
}
}
If you are using your own DAO then you would have to make sure it is configured using #DatabaseTable(daoClass = StationDaoImpl.class).