Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Proposal] Correctly encode/decode multi level type hierarchy with multiple labels. #1728

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -172,11 +173,12 @@ public class PolymorphicJsonAdapterFactory<T> internal constructor(
return null
}
val jsonAdapters: List<JsonAdapter<Any>> = 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<String>,
private val subtypes: List<Type>,
Expand Down Expand Up @@ -223,28 +225,88 @@ public class PolymorphicJsonAdapterFactory<T> 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<Any> = 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)
writer.endFlatten(flattenToken)
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<Pair<PolymorphicJsonAdapter, Int>>): 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<Pair<String, String>>,
val descendantJsonAdapter: JsonAdapter<Any>,
) {

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<OperationSystem> 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
}
}