moshi custom qualifier annotation to serialise null on one property only
Asked Answered
C

2

10

I'd like to serialise null for only one property in my JSON body that is going on a PUT. I don't want to serialize null for any other types in the object. Model class is like this

@Parcel
class User @ParcelConstructor constructor(var college: College?,
                                          var firstname: String?,
                                          var lastname: String?,
                                          var email: String?,
                                          var active: Boolean = true,
                                          var updatedAt: String?,
                                          var gender: String?,
                                          var picture: String?,
                                          var id: String?,
                                          @field: [CollegeField] var collegeInput: String?,
                                          @field: [CollegeField] var otherCollege: String?,)

I only want to serialise collegeInput and otherCollege fields if either of them are null. For example

val user = User(firstname = "foo", lastname=null, collegeInput="abcd", otherCollege = null)

Json will look something like this:

{"user":{
  "firstname": "foo",
  "collegeInput": "abcd",
  "otherCollege": null
}}

Where otherCollege is null, lastname is omitted from the object as by default moshi does not serialise nulls which is what I want, but qualifer fields should be serialized with null values

I tried using

class UserAdapter {
@FromJson
@CollegeField
@Throws(Exception::class)
fun fromJson(reader: JsonReader): String? {
    return when (reader.peek()) {
        JsonReader.Token.NULL ->
            reader.nextNull()
        JsonReader.Token.STRING -> reader.nextString()
        else -> {
            reader.skipValue() // or throw
            null
        }
    }
}

@ToJson
@Throws(IOException::class)
fun toJson(@CollegeField b: String?): String? {
    return b
}


@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class CollegeField

I added the adapter to moshi but it never gets called

@Provides
@Singleton
fun provideMoshi(): Moshi {
    return Moshi.Builder()
            .add(UserAdapter())
            .build()
}

@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient, moshi: Moshi, apiConfig: ApiConfig): Retrofit {
    return Retrofit.Builder()
            .baseUrl(apiConfig.baseUrl)
            .client(client)
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .addConverterFactory(ScalarsConverterFactory.create())
            .addConverterFactory(MoshiConverterFactory.create(moshi))
            .build()
}
Chalice answered 10/9, 2018 at 9:34 Comment(1)
Why your toJson method accepts only one parameter instead of the whole User entity and fromJson returns String and not User?Silvern
B
16

Your toJson adapter method will return null when the qualified string value is null, and the JsonWriter will not write the null value.

Here is a qualifier and adapter factory to install that will work.

@Retention(RUNTIME)
@JsonQualifier
public @interface SerializeNulls {
  JsonAdapter.Factory JSON_ADAPTER_FACTORY = new JsonAdapter.Factory() {
    @Nullable @Override
    public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) {
      Set<? extends Annotation> nextAnnotations =
          Types.nextAnnotations(annotations, SerializeNulls.class);
      if (nextAnnotations == null) {
        return null;
      }
      return moshi.nextAdapter(this, type, nextAnnotations).serializeNulls();
    }
  };
}

Now, the following will pass.

class User(
  var firstname: String?,
  var lastname: String?,
  @SerializeNulls var collegeInput: String?,
  @SerializeNulls var otherCollege: String?
)

@Test fun serializeNullsQualifier() {
  val moshi = Moshi.Builder()
      .add(SerializeNulls.JSON_ADAPTER_FACTORY)
      .add(KotlinJsonAdapterFactory())
      .build()
  val userAdapter = moshi.adapter(User::class.java)
  val user = User(
      firstname = "foo",
      lastname = null,
      collegeInput = "abcd",
      otherCollege = null
  )
  assertThat(
      userAdapter.toJson(user)
  ).isEqualTo(
      """{"firstname":"foo","collegeInput":"abcd","otherCollege":null}"""
  )
}

Note that you should use the Kotlin support in Moshi to avoid the @field: oddities.

Blount answered 10/9, 2018 at 21:29 Comment(3)
This is working for me but I noticed a side effect is that if you have nested objects and you add @SerializeNulls to it not only will it serialize that object to null if it is not set will also serialize all of the null fields inside the nested object. Is it possible to get this to work only at the parent level and to leave the fields inside the nested object alone?Grasshopper
I created this issue that might more clearly show what I am running into. I think the issue is that once the adapter is set to serializeNulls() it remains set to that for the remainder of the objects in that node. #67714356Grasshopper
It shows up "Backend Internal error: Exception during IR lowering" when trying to compile.Turnage
C
0

Try approach from my gist:

https://gist.github.com/OleksandrKucherenko/ffb2126d37778b88fca3774f1666ce66

In my case I convert NULL from JSON into default double/integer value. You can easily modify the approach and make it work for your specific case.

p.s. its JAVA, convert it to Kotlin first.

Christabelle answered 10/9, 2018 at 12:14 Comment(3)
for release build with enabled proguard don't forget to apply Moshi specific rules, plus.google.com/u/0/+OleksandrKucherenko/posts/1wLwv728FgFChristabelle
will test and let you knowChalice
I tried the approach given here github.com/square/moshi. Alternate type adapters with @JsonQualifierChalice

© 2022 - 2024 — McMap. All rights reserved.