Search code examples
androidmoshi

Passing information between Moshi custom type adaptors


I am using Moshi to deserialize json from our server but I have come across an issue I’m sure has a solution, I just can’t see it. Over the socket, we are send json that, at the top level, has three fields:

{
    "data_type": "<actual_data_type>",
    "data_id": "<actual_data_id>",
    "data": <data_object>
}

The issue is that the data can actually be several different objects based on what data_type is can I’m not sure how to pass that information into the adaptor for Data. I’ve tried a couple different things, but it just gets closer and closer to me parsing the whole thing myself, which seems to defeat the point. Is there a way to pass information from one adaptor to another?


Solution

  • For anyone who wants to do something similar, I took the basic shape of a generic factory from here: https://github.com/square/moshi/pull/264/files (which also what @eric cochran is recommending in his comment) and made it more specific to fit my exact case.

    class EventResponseAdapterFactory : JsonAdapter.Factory {
        private val labelKey = "data_type"
        private val subtypeToLabel = hashMapOf<String, Class<out BaseData>>(
            DataType.CURRENT_POWER.toString() to CurrentPower::class.java,
            DataType.DEVICE_STATUS_CHANGED.toString() to DeviceStatus::class.java,
            DataType.EPISODE_EVENT.toString() to EpisodeEvent::class.java,
            DataType.APPLIANCE_INSTANCE_UPDATED.toString() to ApplianceInstanceUpdated::class.java,
            DataType.RECURRING_PATTERNS.toString() to RecurringPatternOccurrence::class.java,
            DataType.RECURRING_PATTERN_UPDATED.toString() to RecurringPatternUpdated::class.java
        )
    
        override fun create(type: Type, annotations: Set<Annotation>, moshi: Moshi): JsonAdapter<*>? {
            if (!annotations.isEmpty() || type != EventResponse::class.java) {
                return null
            }
    
            val size = subtypeToLabel.size
            val labelToDelegate = LinkedHashMap<String, JsonAdapter<EventResponse<BaseData>>>(size)
            for (entry in subtypeToLabel.entries) {
                val key = entry.key
                val value = entry.value
                val parameterizedType = Types.newParameterizedType(EventResponse::class.java, value)
                val delegate = moshi.adapter<EventResponse<BaseData>>(parameterizedType, annotations)
                labelToDelegate.put(key, delegate)
            }
    
            return EventResponseAdapter(
                labelKey,
                labelToDelegate
            )
        }
    
        private class EventResponseAdapter internal constructor(
            private val labelKey: String,
            private val labelToDelegate: LinkedHashMap<String, JsonAdapter<EventResponse<BaseData>>>
        ) : JsonAdapter<EventResponse<BaseData>>() {
    
            override fun fromJson(reader: JsonReader): EventResponse<BaseData>? {
                val raw = reader.readJsonValue()
                if (raw !is Map<*, *>) {
                    throw JsonDataException("Value must be a JSON object but had a value of $raw of type ${raw?.javaClass}")
                }
    
                val label = raw.get(labelKey) ?: throw JsonDataException("Missing label for $labelKey")
                if (label !is String) {
                    throw JsonDataException("Label for $labelKey must be a string but had a value of $label of type ${label.javaClass}")
                }
                val delegate = labelToDelegate[label] ?: return null
                return delegate.fromJsonValue(raw)
            }
    
            // Not used
            override fun toJson(writer: JsonWriter, value: EventResponse<BaseData>?) {}
        }
    }
    

    The only thing to watch out for is that the RuntimeJsonAdapterFactory in the link uses Types.getRawType(type) to get the type with the generics stripped away. We, of course, don't want that because once the specific generic type has been found, we want the normal Moshi adapters to kick in and do the proper parsing for us.