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

Offer payload pattern for polymorphic adapters #164

Open
ZacSweers opened this issue Nov 21, 2021 · 3 comments
Open

Offer payload pattern for polymorphic adapters #164

ZacSweers opened this issue Nov 21, 2021 · 3 comments
Labels
enhancement New feature or request

Comments

@ZacSweers
Copy link
Owner

ZacSweers commented Nov 21, 2021

Moshi's polymorphic adapter works just for the tag pattern, but another common one is the tag payload pattern (Reddit and GitHub both do this).

{
  "type": "someType",
  "payload": {
    # Based on "type"
  }
}

Where payload is based on someType's type.

This is easy to do in simple cases where the type and payload are the only values. It's much harder to do it for cases where they're mixed in with some other type though, like so

data class GitHubActivityEvent(
  val id: String,
  val createdAt: Instant,
  val type: PayloadType,
  val payload: GitHubActivityEventPayload?,
  val public: Boolean,
  val repo: Repo?
)

I think a clever solution in this case would be a two-stage adapter. Stage one would find the type, store it in the reader, then delegate to the "real" adapter and make the generated payload adapter look for this breadcrumb. In effect, this would mean generating two adapters. One that takes control of GitHubActivityEvent and one that handles GitHubActivityEventPayload. This likely requires an extra adapter to look for these "type holder" types to handle delegation safely.

@PayloadType("type", PayloadType::class) // Marker
data class GitHubActivityEvent(
  val type: PayloadType,
  val payload: GitHubActivityEventPayload?,
)

@JsonClass(generateAdapter = true, generator = "payload")
sealed class GitHubActivityEventPayload(...)

class PayloadTypeJsonAdapterFactory : JsonAdapter.Factory {
  override fun create(...): JsonAdapter<*>? {
    // Look for the PayloadType annotation
  }
  
  override fun fromJson(reader: JsonReader) {
    // Peek ahead, find the type
    val type = ...
    reader.setTag(PayloadTypeHint::class.java, PayloadTypeHint(typeValue))
    return delegate.fromJson(reader)
  }
}

// In a generated payload adapter
class GitHubActivityEventPayloadJsonAdapter : JsonAdapter<GitHubActivityEventPayload>() {
  // Delegate adapters

  override fun fromJson(...): GitHubActivityEventPayload? {
    val type = reader.tag(PayloadTypeHint::class.java)?.value
    reader.setTag(PayloadTypeHint::class.java, null)
    return switch (type) {
      // ...
      null -> // Use either default null or default object
    }
  }
}
@ZacSweers ZacSweers added the enhancement New feature or request label Nov 21, 2021
@jD4rk
Copy link

jD4rk commented Jul 5, 2022

This would solve my actual problem Im currently struggle with.

I do have kinda of nested json to parse:
there is a key that could be different (but known) value,
and based on those value inside the json there is a object that have to parse into different data class based on the value of the key above.

It's kind of nested /multi level polymorphic adapter.

At the moment the way I managed it, is with a custom adapter that thanks to reader.peekJson() make kind of raw parsing to figure out that is the value of the key that discriminate the type of the data class that has to be used.
And then parse the object according on that.

@JsonClass(generateAdapter = true)
data class OuterClass @JvmOverloads constructor(
    val id : Int,
    val actionType: String,
    val createdAt: String,
    @Json(ignore = true)
    val params: TypedParams = TypedParams.Empty,
)

sealed interface TypedParams {
    @JsonClass(generateAdapter = true)
    data class Type1(
        val someInt : Int = -1
        val someBooelan: Boolean = false,
    ): ActionParams
 
    @JsonClass(generateAdapter = true)
    data class Type2(
        val someString : string = "",
    ): ActionParams
}

I currently take advantage of the annotation @JSON(ignore = true) in the outer class to parse all the other value with the generated adapter, and into my custom one, perform kind of two way parsing exploring the json until i found the key that is ignored and then parsing using the "inner" generated adapter on the object using a when statement

                         when (type) {
                             "Type1" -> {
                                 params = type1Adapter.fromJson(reader)!!
                             }
                             "Type2" -> {
                                 params = type2Adapter.fromJson(reader)!!
                             }

What I don't really like with this approach is that I need to manually add a new check case for the when statement inside the adapter every time a new Types is added into the json response
A sort of annotation that handle this situation and generate the whole adapter avoid to use "ignore" annotation and automatically perform the polymorphic parse on it own would be lovely!

@troyjperales
Copy link

This is easy to do in simple cases where the type and payload are the only values.

@ZacSweers Is this simple case achievable using only moshi-sealed or is a manual JsonAdapter/Factory required?

@ZacSweers
Copy link
Owner Author

This issue is open because it's not supported currently

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants