I'm writing an SDK for a JSON API and I'm running into a seemingly weird problem. The API is quite strict in it's POST data validation and it does not allow certain parameters to be present when updating resources, like an id. For this reason, I added @Expose(serialize = false) the ID field of my resource class. It seems however, that it still serializes this field, causing the request to be rejected. The resource class roughly is as follows:
public class Organisation extends BaseObject
{
public static final Gson PRETTY_PRINT_JSON = new GsonBuilder()
.setPrettyPrinting()
.create();
@Expose(serialize = false)
@SerializedName("_id")
private String id;
@SerializedName("email")
private String email;
@SerializedName("name")
private String name;
@SerializedName("parent_id")
private String parentId;
public String toJson()
{
return PRETTY_PRINT_JSON.toJson(this);
}
}
My unit tests create an instance of Organisation via the API, saves the newly created instance to the test class as a class parameter and calls an update method that will test the update implementation of the SDK by updating the new resource. This is where it goes wrong. Even though the toJson() method is called on the new Organisation to serialise it to JSON for the update request, the _id field remains present, causing the API to decline the update. The test code is as follows. Notice the comments in the code.
@Test
public void testCreateUpdateAndDeleteOrganisation() throws RequestException
{
Organisation organisation = new Organisation();
organisation.setParentId(this.ORGANISATION_ID);
organisation.setName("Java Test Organisation");
Organisation newOrganisation = this.MySDK.organisation.create(organisation);
this.testOrganisation(newOrganisation);
this.newOrganisation = newOrganisation;
this.testUpdateOrganisation();
}
public void testUpdateOrganisation() throws RequestException
{
// I tried setting ID to null, but that doesn't work either
// even though I've set Gson to not serialise null values
this.newOrganisation.setId(null);
this.newOrganisation.setName(this.newName);
// For debugging
System.out.println(this.newOrganisation.toJson());
Organisation updatedOrganisation = this.MySDK.organisation.update(this.newOrganisation.getId(), this.newOrganisation);
this.testOrganisation(updatedOrganisation);
assertEquals(newOrganisation.getName(), this.newName);
this.testDeleteOrganisation();
}
Can anyone spot what I'm doing wrong? I have a feeling it has something to do with the fact that the instance already has/had a value for ID, but that shouldn't matter if I explicity tell it not to serialise it right?
Thanks in advance for the help.
EDIT: At this.MySDK.organisation.update(this.newOrganisation.getId(), this.newOrganisation);, that does not edit the organisation instance. The given ID is merely added to the URL that the SDK will POST to (POST /organisation/{id})
Thanks to @peitek for pointing out that @Expose is ignored unless .excludeFieldsWithoutExposeAnnotation() is added to the GsonBuilder(). However, I choose not to go with this route as it would require me to add @Expose to every single parameter of my model classes just to ignore one field on serialisation. Instead I wrote a ExclusionStrategy that checks for the presence of a custom SkipSerialisation annotation on the parameter. I implemented these as follows:
The full GsonBuilder with the strategy:
public static final Gson PRETTY_PRINT_JSON = new GsonBuilder()
.addSerializationExclusionStrategy(new ExclusionStrategy()
{
@Override
public boolean shouldSkipField(FieldAttributes f)
{
return f.getAnnotation(SkipSerialisation.class) != null;
}
@Override
public boolean shouldSkipClass(Class<?> clazz)
{
return false;
}
})
.setPrettyPrinting()
.create();
And the annotation:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface SkipSerialisation
{
}
Now I can just do
@SkipSerialisation
@SerializedName("_id")
private String id;
and it works!
As you mentioned in your comments, @Expose should be the better choice over transient here. It's important to note that the default Gson instance does not regard the @Expose annotation! It'll simply ignore it, no matter what you set as option.
If you want to activate the @Expose options, you need to customize Gson. Based on your code above, change it to:
public static final Gson PRETTY_PRINT_JSON = new GsonBuilder()
.setPrettyPrinting()
.excludeFieldsWithoutExposeAnnotation();
.create();
Your @Expose(serialize = false) should be active and excluded during serialization.
I just wanted to add here that you could use Expose in the desired way like this:
builder.addSerializationExclusionStrategy(new ExclusionStrategy() {
@Override
public boolean shouldSkipField(FieldAttributes f) {
Expose annotation = f.getAnnotation(Expose.class);
if(annotation != null)
return !annotation.serialize();
else
return false;
}
@Override
public boolean shouldSkipClass(Class<?> clazz) {
Expose annotation = clazz.getAnnotation(Expose.class);
if(annotation != null)
return !annotation.serialize();
else
return false;
}
});
builder.addDeserializationExclusionStrategy(new ExclusionStrategy() {
@Override
public boolean shouldSkipField(FieldAttributes f) {
Expose annotation = f.getAnnotation(Expose.class);
if(annotation != null)
return !annotation.deserialize();
else
return false;
}
@Override
public boolean shouldSkipClass(Class<?> clazz) {
Expose annotation = clazz.getAnnotation(Expose.class);
if(annotation != null)
return !annotation.deserialize();
else
return false;
}
});