Using an annotation to Serialize Null in Moshi with nested Event
Asked Answered
A

3

4

I am attempting to add a custom annotation to serialize specific values in my model to null when calling the toJSON method from Moshi. I have something working based on this response but it's falling short for me when I have a nested object.

@JsonClass(generateAdapter = true)
data class EventWrapper(
    @SerializeNulls val event: Event?,
    @SerializeNulls val queries: Queries? = null) {

    @JsonClass(generateAdapter = true)
    data class Queries(val stub: String?)

    @JsonClass(generateAdapter = true)
    data class Event(
       val action: String?,
       val itemAction: String)
}

If I pass null to event or queries they are serialized as:

{
    'event': null,
    'query': null
}

The issue is when event isn't null there are fields inside of it I would like to not serialize if they are null such as action. My preferred result would be this:

{
    'event': {
        'itemAction': "test" 
    },
    'query': null
}

But instead I am getting:

{
    'event': {
        'action': null,
        'itemAction': "test" 
    },
    'query': null
}

Here is the code for my custom adapter based on the linked response:

@Retention(RetentionPolicy.RUNTIME)
 @JsonQualifier
 annotation class SerializeNulls {
     companion object {
         var JSON_ADAPTER_FACTORY: JsonAdapter.Factory = object : JsonAdapter.Factory {

             @RequiresApi(api = Build.VERSION_CODES.P)
             override fun create(type: Type, annotations: Set<Annotation?>, moshi: Moshi): JsonAdapter<*>? {
                 val nextAnnotations = Types.nextAnnotations(annotations, SerializeNulls::class.java)

                 return if (nextAnnotations == null) {
                     null
                 } else {
                     moshi.nextAdapter<Any>(this, type, nextAnnotations).serializeNulls()
                 }
        }
    }
}
Attenuation answered 26/5, 2021 at 23:19 Comment(0)
Y
3

I've had the same issue and the only solution I found was to make a custom adapter instead of using the SerializeNulls annotation. This way, it will only serialize nulls if the object is null, and serialize it normally with the generated adapter otherwise.

class EventJsonAdapter {
    private val adapter = Moshi.Builder().build().adapter(Event::class.java)

    @ToJson
    fun toJson(writer: JsonWriter, event: Event?) {
        if (event == null) {
            with(writer) {
                serializeNulls = true
                nullValue()
                serializeNulls = false
            }
        } else {
            adapter.toJson(writer, event)
        }
    }
}

For the generated adapter to work don't forget to annotate the Event class with:

@JsonClass(generateAdapter = true)

The custom adapter can then be added to the moshi builder like this:

Moshi.Builder().add(EventJsonAdapter()).build()

In my case I only needed this for one model in specific. Probably not a good solution if you need it for several, in which case the annotation is more practical, but I'll leave it here since it might help someone else.

Yonyona answered 28/7, 2021 at 15:56 Comment(0)
L
3

@Su-Au Hwang's answer in thread worked for me. Which is better than creating custom adapters for our models. Thanks.

Here is same code with Kotlin:

  1. Create a new adapter NullIfNullJsonAdapter

    class NullIfNullJsonAdapter<T>(val delegate: JsonAdapter<T>) : JsonAdapter<T>() {
    
            override fun fromJson(reader: JsonReader): T? {
                return delegate.fromJson(reader)
            }
    
            override fun toJson(writer: JsonWriter, value: T?) {
                if (value == null) {
                    val serializeNulls: Boolean = writer.serializeNulls
                    writer.serializeNulls = true
                    try {
                        delegate.toJson(writer, value)
                    } finally {
                        writer.serializeNulls = serializeNulls
                    }
                } else {
                    delegate.toJson(writer, value)
                }
            }
    
            override fun toString(): String {
                return "$delegate.serializeNulls()"
            }
        }
    
  2. Create JsonQualifier that you can use for annotation

     @Retention(AnnotationRetention.RUNTIME)
     @JsonQualifier
     annotation class SerializeNull {
         companion object {
             object Factory : JsonAdapter.Factory {
                 override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
                     val nextAnnotations = Types.nextAnnotations(annotations, SerializeNull::class.java)
    
                     return if (nextAnnotations == null) {
                         null
                     } else {
                         NullIfNullJsonAdapter<Any>(moshi.nextAdapter(this, type, nextAnnotations))
                     }
                 }
             }
         }
     }
    
  3. Add Factory to moshi builder

     val moshi = Moshi.Builder()
                 .add(SerializeNull.Companion.Factory)
                 .build()
    
  4. Annotate property with @SerializeNull (e.g. host)

     @JsonClass(generateAdapter = true)
     data class Event(
          val name: String,
          @SerializeNull val host: Host?,
          val venue: String?,
      )
    

Some tests

 @JsonClass(generateAdapter = true)
 data class Host(val firstName: String, val lastName: String?)

 @JsonClass(generateAdapter = true)
 data class Event(
     val name: String,
     @SerializeNull val host: Host?,
     val venue: String?,
 )


 @Test
 fun `everything is added to json if object has everything`() {
     val moshi = Moshi.Builder()
         .add(SerializeNull.Companion.Factory)
         .build()
     val jsonAdapter: JsonAdapter<Event> = moshi.adapter()
     val event = Event(
         name = "Birthday Party",
         host = Host(firstName = "Harsh", lastName = "Bhakta"),
         venue = "House"
     )
     val json = jsonAdapter.toJson(event)
     assertEquals("""{"name":"Birthday Party","host":{"firstName":"Harsh","lastName":"Bhakta"},"venue":"House"}""", json)
 }

 @Test
 fun `check host  is null in json since host is annotated with serializeNull and venue is excluded because it's not annotated`() {
     val moshi = Moshi.Builder()
         .add(SerializeNull.Companion.Factory)
         .build()
     val jsonAdapter: JsonAdapter<Event> = moshi.adapter()
     val event = Event(
         name = "Birthday Party",
         host = null,
         venue = null
     )
     val json = jsonAdapter.toJson(event)
     assertEquals("""{"name":"Birthday Party","host":null}""", json)
 }

 @Test
 fun `check nested property (host lastname) is not included if it's null because it's not annotated with SerializeNull`() {
     val moshi = Moshi.Builder()
         .add(SerializeNull.Companion.Factory)
         .build()
     val jsonAdapter: JsonAdapter<Event> = moshi.adapter()
     val event = Event(
         name = "Birthday Party",
         host = Host(firstName = "Harsh", lastName = null),
         venue = null
     )
     val json = jsonAdapter.toJson(event)
     assertEquals("""{"name":"Birthday Party","host":{"firstName":"Harsh"}}""", json)
 }

Linderman answered 14/4, 2022 at 21:49 Comment(0)
S
2

The issue is .serializeNulls() returns an adapter that serializes nulls all the way down the tree.

You can simply copy the implementation for .serializeNulls() and add null check in the toJson method and only use it if it's null like this (sorry for my Java):

import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;

import java.io.IOException;

import javax.annotation.Nullable;

public class NullIfNullJsonAdapter<T> extends JsonAdapter<T> {
    final JsonAdapter<T> delegate;

    public NullIfNullJsonAdapter(JsonAdapter<T> delegate) {
        this.delegate = delegate;
    }

    @Override
    public @Nullable
    T fromJson(JsonReader reader) throws IOException {
        return delegate.fromJson(reader);
    }

    @Override
    public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
        if (value == null) {
            boolean serializeNulls = writer.getSerializeNulls();
            writer.setSerializeNulls(true);
            try {
                delegate.toJson(writer, value);
            } finally {
                writer.setSerializeNulls(serializeNulls);
            }
        } else {
            delegate.toJson(writer, value);
        }
    }

    @Override
    public String toString() {
        return delegate + ".serializeNulls()";
    }
}

and then you can use it in the @JsonQualifier

import static java.lang.annotation.RetentionPolicy.RUNTIME;

import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonQualifier;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;

import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.reflect.Type;
import java.util.Set;

import javax.annotation.Nullable;

@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 new NullIfNullJsonAdapter(moshi.nextAdapter(this, type, nextAnnotations));
        }
    };
}
Sharisharia answered 11/11, 2021 at 9:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.