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()
}
Jaskaran Singh
  • 113
  • 1
  • 5
  • Why your `toJson` method accepts only one parameter instead of the whole `User` entity and `fromJson` returns `String` and not `User`? – Praytic Sep 10 '18 at 11:38

2 Answers2

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.

Eric Cochran
  • 8,414
  • 5
  • 50
  • 91
  • 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? – Bueno May 26 '21 at 17:25
  • 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. https://stackoverflow.com/questions/67713856/using-an-annotation-to-serialize-null-in-moshi-with-nested-event – Bueno May 27 '21 at 12:47
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.

Oleksandr Kucherenko
  • 1,921
  • 1
  • 17
  • 21