'Using an annotation to Serialize Null in Moshi with nested Event
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()
}
}
}
}
Solution 1:[1]
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.
Solution 2:[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));
}
};
}
Solution 3:[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:
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()" } }
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)) } } } } }
Add Factory to moshi builder
val moshi = Moshi.Builder() .add(SerializeNull.Companion.Factory) .build()
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)
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Cristina Martins |
Solution 2 | Su-Au Hwang |
Solution 3 |