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?
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.