diff --git a/moshi-adapters/src/main/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactory.kt b/moshi-adapters/src/main/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactory.kt index 74e5b8304..82b63feed 100644 --- a/moshi-adapters/src/main/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactory.kt +++ b/moshi-adapters/src/main/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactory.kt @@ -22,6 +22,7 @@ import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonReader.Options import com.squareup.moshi.JsonWriter import com.squareup.moshi.Moshi +import com.squareup.moshi.internal.NullSafeJsonAdapter import com.squareup.moshi.rawType import okio.IOException import java.lang.reflect.Type @@ -172,11 +173,12 @@ public class PolymorphicJsonAdapterFactory internal constructor( return null } val jsonAdapters: List> = subtypes.map(moshi::adapter) - return PolymorphicJsonAdapter(labelKey, labels, subtypes, jsonAdapters, fallbackJsonAdapter) + return PolymorphicJsonAdapter(baseType, labelKey, labels, subtypes, jsonAdapters, fallbackJsonAdapter) .nullSafe() } internal class PolymorphicJsonAdapter( + private val baseType: Class<*>, private val labelKey: String, private val labels: List, private val subtypes: List, @@ -223,16 +225,21 @@ public class PolymorphicJsonAdapterFactory internal constructor( override fun toJson(writer: JsonWriter, value: Any?) { val type: Class<*> = value!!.javaClass val labelIndex = subtypes.indexOf(type) + val descendantInfo: DescendantInfo? val adapter: JsonAdapter = if (labelIndex == -1) { - requireNotNull(fallbackJsonAdapter) { + descendantInfo = findDescendantInfo(type, emptyList()) + descendantInfo?.descendantJsonAdapter ?: requireNotNull(fallbackJsonAdapter) { "Expected one of $subtypes but found $value, a ${value.javaClass}. Register this subtype." } } else { + descendantInfo = null jsonAdapters[labelIndex] } writer.beginObject() - if (adapter !== fallbackJsonAdapter) { + if (descendantInfo == null && adapter !== fallbackJsonAdapter) { writer.name(labelKey).value(labels[labelIndex]) + } else { + descendantInfo?.writeLabels(writer) } val flattenToken = writer.beginFlatten() adapter.toJson(writer, value) @@ -240,11 +247,66 @@ public class PolymorphicJsonAdapterFactory internal constructor( writer.endObject() } + /** + * When [type] is not a direct child of [baseType], recursively search for the descendant. + * + * @param history This method runs a depth-first search through the type hierarchy. Once it reaches a + * [PolymorphicJsonAdapter], it continues the search from that adapter. This parameter tracks the pair of that + * [PolymorphicJsonAdapter] and the index of the subtype [PolymorphicJsonAdapter] of it. + */ + private fun findDescendantInfo(type: Type, history: List>): DescendantInfo? = + jsonAdapters + .asSequence() + // The pairs of [PolymorphicJsonAdapter]-compatible [JsonAdapter], and the index of it in the [jsonAdapters] list. + .mapIndexedNotNull { index, adapter -> + if (adapter is PolymorphicJsonAdapter) { + index to adapter + } else if (adapter is NullSafeJsonAdapter<*>) { + val delegate = adapter.delegate + if (delegate is PolymorphicJsonAdapter) { + index to delegate + } else { + null + } + } else { + null + } + } + // Traverse the [PolymorphicJsonAdapter] and its index and find a direct descendant or continue the search. + .firstNotNullOfOrNull { (index, adapter) -> + val typeIndex = adapter.subtypes.indexOf(type) + if (typeIndex != -1) { + DescendantInfo( + history = history + .plus(this to index) + .mapNotNull { (baseTypeAdapter, directAncestorIndex) -> + (baseTypeAdapter.labelKey to baseTypeAdapter.labels[directAncestorIndex]) + .takeIf { baseTypeAdapter.baseType.isInterface } + }, + descendantJsonAdapter = adapter.jsonAdapters[typeIndex], + ) + } else { + adapter.findDescendantInfo(type, history.plus(this to index)) + } + } + override fun toString(): String { return "PolymorphicJsonAdapter($labelKey)" } } + private class DescendantInfo( + val history: List>, + val descendantJsonAdapter: JsonAdapter, + ) { + + fun writeLabels(writer: JsonWriter) { + history.forEach { (key, label) -> + writer.name(key).value(label) + } + } + } + public companion object { /** * @param baseType The base type for which this factory will create adapters. Cannot be Object. diff --git a/moshi-adapters/src/test/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactoryTest.java b/moshi-adapters/src/test/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactoryTest.java index 68011b82c..3706ac580 100644 --- a/moshi-adapters/src/test/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactoryTest.java +++ b/moshi-adapters/src/test/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactoryTest.java @@ -445,4 +445,118 @@ static final class MessageWithType implements Message { this.value = value; } } + + @Test + public void multiLevelPolymorphic() throws IOException { + Moshi moshi = + new Moshi.Builder() + .add( + PolymorphicJsonAdapterFactory.of(OperationSystem.class, "familyName") + .withSubtype(Windows.class, "Windows") + .withSubtype(MacOS.class, "MacOS") + .withSubtype(Others.class, "Others")) + .add( + PolymorphicJsonAdapterFactory.of(Others.class, "projectType") + .withSubtype(OpenSourceSystem.class, "OPEN_SOURCE") + .withSubtype(ClosedSourceSystem.class, "CLOSED_SOURCE") + .withSubtype(Unknown.class, "UNLICENSED")) + .add( + PolymorphicJsonAdapterFactory.of(OpenSourceSystem.class, "name") + .withSubtype(Linux.class, "Linux") + .withSubtype(Android.class, "Android")) + .add( + PolymorphicJsonAdapterFactory.of(ClosedSourceSystem.class, "name") + .withSubtype(IOS.class, "iOS")) + .build(); + + JsonAdapter adapter = moshi.adapter(OperationSystem.class); + assertThat( + adapter.fromJson( + "{\"familyName\":\"Others\",\"projectType\":\"OPEN_SOURCE\",\"name\":\"Linux\"}")) + .isInstanceOf(Linux.class); + assertThat( + adapter.fromJson( + "{\"familyName\":\"Others\",\"projectType\":\"CLOSED_SOURCE\",\"name\":\"iOS\",\"latestMajorVersion\":12}")) + .isInstanceOf(IOS.class); + assertThat(adapter.fromJson("{\"familyName\":\"Others\",\"projectType\":\"UNLICENSED\"}")) + .isInstanceOf(Unknown.class); + + assertThat(adapter.toJson(new MacOS())).isEqualTo("{\"familyName\":\"MacOS\"}"); + + assertThat(adapter.toJson(new Unknown())) + .isEqualTo("{\"familyName\":\"Others\",\"projectType\":\"UNLICENSED\"}"); + + assertThat(adapter.toJson(new IOS(12))) + .isEqualTo( + "{\"familyName\":\"Others\",\"latestMajorVersion\":12,\"name\":\"iOS\",\"projectType\":\"CLOSED_SOURCE\"}"); + } + + interface OperationSystem {} + + static final class Windows implements OperationSystem {} + + static final class MacOS implements OperationSystem {} + + abstract static class Others implements OperationSystem { + final ProjectType projectType; + + Others(ProjectType projectType) { + this.projectType = projectType; + } + } + + static class Unknown extends Others { + + Unknown() { + super(ProjectType.UNLICENSED); + } + } + + abstract static class OpenSourceSystem extends Others { + + final String name; + + OpenSourceSystem(String name) { + super(ProjectType.OPEN_SOURCE); + this.name = name; + } + } + + static final class Linux extends OpenSourceSystem { + Linux() { + super("Linux"); + } + } + + static final class Android extends OpenSourceSystem { + Android() { + super("Android"); + } + } + + abstract static class ClosedSourceSystem extends Others { + + final String name; + + ClosedSourceSystem(String name) { + super(ProjectType.CLOSED_SOURCE); + this.name = name; + } + } + + static final class IOS extends ClosedSourceSystem { + + final int latestMajorVersion; + + IOS(int latestMajorVersion) { + super("iOS"); + this.latestMajorVersion = latestMajorVersion; + } + } + + enum ProjectType { + OPEN_SOURCE, + CLOSED_SOURCE, + UNLICENSED + } }