Search code examples
androidkotlinmoshi

How to encode a field of an object as stringifed JSON instead of nested JSON object in Moshi?


I have a sealed class WebSocketMessage which has some subclasses. The WebSocketMessage has a field named type which is used for differentiating between subclasses.

All of the subclasses have their own field named payload which is of different type for each subclass.

Currently I am using Moshi's PolymorphicJsonAdapterFactory so that these classes can be parsed from JSON and encoded to JSON.

This all works, but what I need is to encode the the payload field to stringified JSON instead of JSON object.

Is there any possibility to write a custom adapter class to help me with this problem? Or is there any other solution so that I will not have to do this stringification manually?

I have tried looking into custom adapters but I can't find how I could pass moshi instance to adapter so that I can encode the given field to JSON and then stringify it, nor did I find anything else that could help me.

The WebSocketMessage class with its subclasses:


sealed class WebSocketMessage(
    val type: Type
) {
    enum class Type(val type: String) {
        AUTH("AUTH"),
        PING("PING"),
        FLOW_INITIALIZATION("FLOW_INITIALIZATION")
    }

    class Ping : WebSocketMessage(Type.PING)
    class InitFlow(payload: InitFlowMessage) : WebSocketMessage(Type.FLOW_INITIALIZATION)
    class Auth(payload: Token) : WebSocketMessage(Type.AUTH)
}

The Moshi instance with PolymorphicJsonAdapterFactory:

val moshi = Moshi.Builder().add(
                    PolymorphicJsonAdapterFactory.of(WebSocketMessage::class.java, "type")
                        .withSubtype(WebSocketMessage.Ping::class.java, WebSocketMessage.Type.PING.type)
                        .withSubtype(
                            WebSocketMessage.InitFlow::class.java,
                            WebSocketMessage.Type.FLOW_INITIALIZATION.type
                        )                        
                        .withSubtype(WebSocketMessage.Auth::class.java, WebSocketMessage.Type.AUTH.type)
                )
                // Must be added last
                .add(KotlinJsonAdapterFactory())
                .build()

How I encode to JSON:

moshi.adapter(WebSocketMessage::class.java).toJson(WebSocketMessage.Auth(fetchToken()))

I currently get the JSON in the next format:

{  
   "type":"AUTH",
   "payload":{  
      "jwt":"some_token"
   }
}

What I would like to get:

{  
   "type":"AUTH",
   "payload":"{\"jwt\":\"some_token\"}"
}

In the second example the payload is a stringified JSON object, which is exactly what I need.


Solution

  • You can create your own custom JsonAdapter:

    @Retention(AnnotationRetention.RUNTIME)
    @JsonQualifier
    annotation class AsString
    
    /////////////////////
    
    class AsStringAdapter<T>(
        private val originAdapter: JsonAdapter<T>,
        private val stringAdapter: JsonAdapter<String>
    ) : JsonAdapter<T>() {
    
        companion object {
    
            var FACTORY: JsonAdapter.Factory = object : Factory {
                override fun create(
                    type: Type,
                    annotations: MutableSet<out Annotation>,
                    moshi: Moshi
                ): JsonAdapter<*>? {
                    val nextAnnotations = Types.nextAnnotations(annotations, AsString::class.java)
                    return if (nextAnnotations == null || !nextAnnotations.isEmpty())
                        null else {
                        AsStringAdapter(
                            moshi.nextAdapter<Any>(this, type, nextAnnotations),
                            moshi.nextAdapter<String>(this, String::class.java, Util.NO_ANNOTATIONS)
                        )
                    }
                }
            }
        }
    
        override fun toJson(writer: JsonWriter, value: T?) {
            val jsonValue = originAdapter.toJsonValue(value)
            val jsonStr = JSONObject(jsonValue as Map<*, *>).toString()
            stringAdapter.toJson(writer, jsonStr)
        }
    
        override fun fromJson(reader: JsonReader): T? {
            throw UnsupportedOperationException()
        }
    }
    
    /////////////////////
    
    class Auth(@AsString val payload: Token)
    
    /////////////////////
    
    .add(AsStringAdapter.FACTORY)
    .add(KotlinJsonAdapterFactory())
    .build()