Apollo Kotlin: How to manually serialize the generated data classes from a Query?

I need to (de)serialize to and from JSON some of the data classes that are autogenerated by Apollo Kotlin. Normally I would just use a library like Moshi or kotlinx.serialization and add the proper annotation to the class I want to serialize but, since this data classes are autogenerated, I can’t do that.

But Apollo Kotlin is already (de)serializing them so I was wondering if there is a way to use the internal serialization mechanism from Apollo Kotlin to (de)serialize these autogenerated data classes.

I saw that the Query has already something related to this:

public data class ExpertSearchQuery(
  public val searchString: Optional<String?> = Optional.Absent,
  public val page: Optional<Int?> = Optional.Absent,
  public val pageSize: Optional<Int?> = Optional.Absent,
) : Query<ExpertSearchQuery.Data> {
  public override fun id(): String = OPERATION_ID

  public override fun document(): String = OPERATION_DOCUMENT

  public override fun name(): String = OPERATION_NAME

  public override fun serializeVariables(writer: JsonWriter,
      customScalarAdapters: CustomScalarAdapters): Unit {
    ExpertSearchQuery_VariablesAdapter.toJson(writer, customScalarAdapters, this)
  }

  public override fun adapter(): Adapter<Data> = ExpertSearchQuery_ResponseAdapter.Data.obj()

But I need to (de)serialize only a single class from the above query:

  public data class Expert(
    public val __typename: String,
    public val profileId: String?,
    public val firstName: String?,
    public val middleName: String?,
    public val lastName: String?,
    public val imageUrl: String?,
    public val titles: List<String?>?,
    public val primaryAffiliation: PrimaryAffiliation?,
  )

Is it possible to do this?

Hi :wave:

You can look inside ExpertSearchQuery_ResponseAdapter, there should be an adapter for Expert that you can use with something like this:

ExpertSearchQuery_ResponseAdapter.Expert.obj()
    .fromJson(
        Buffer().writeUtf8(jsonString).jsonReader(), 
        CustomScalarAdapter.Empty
    )

ExpertSearchQuery_ResponseAdapter.Expert.obj()
    .toJson(
        buffer, 
        CustomScalarAdapter.Empty,
        expert
    )

Hi @mbonnin, thanks for the quick answer!

I’m trying to use that adapter but I get this exception:

Process: com.veeva.link.debug, PID: 9354
java.lang.IllegalStateException: Nesting problem.
	at com.apollographql.apollo3.api.json.BufferedSinkJsonWriter.beforeValue(BufferedSinkJsonWriter.kt:255)
	at com.apollographql.apollo3.api.json.BufferedSinkJsonWriter.open(BufferedSinkJsonWriter.kt:100)
	at com.apollographql.apollo3.api.json.BufferedSinkJsonWriter.beginObject(BufferedSinkJsonWriter.kt:89)
	at com.apollographql.apollo3.api.ObjectAdapter.toJson(Adapters.kt:247)

Btw, I’m now trying to serialize a different Query and data class from my topic: ObjectiveQuery_ResponseAdapter.OwnedBy.

The data class looks like this:

  public data class OwnedBy(
    public val id: Int,
    public val username: String,
    public val firstName: String?,
    public val lastName: String?,
    public val image: String?,
    public val __typename: String,
  )

The exception happens because the peekScope() method returns EMPTY_OBJECT here:

  private fun beforeValue() {
    when (peekScope()) {
      JsonScope.NONEMPTY_DOCUMENT -> {
        check(isLenient) { "JSON must have only one top-level value." }
        replaceTop(JsonScope.NONEMPTY_DOCUMENT)
      }
      JsonScope.EMPTY_DOCUMENT -> replaceTop(JsonScope.NONEMPTY_DOCUMENT)
      JsonScope.EMPTY_ARRAY -> {
        replaceTop(JsonScope.NONEMPTY_ARRAY)
        newline()
      }
      JsonScope.NONEMPTY_ARRAY -> {
        sink.writeByte(','.code)
        newline()
      }
      JsonScope.DANGLING_NAME -> {
        sink.writeUtf8(separator)
        replaceTop(JsonScope.NONEMPTY_OBJECT)
      }
      else -> throw IllegalStateException("Nesting problem.")
    }
  }

I’m not sure how to get a proper instance of the JsonWriter, currently I’m doing it like this:

        val buffer = Buffer()
        BufferedSinkJsonWriter(buffer).apply {
            beginObject()
            ObjectiveQuery_ResponseAdapter.OwnedBy.obj().nullable().toJson(this, CustomScalarAdapters.Empty, checkNotNull(viewState.value.ownedBy))
            endObject()
        }
        return buffer.readUtf8()

I tried also to remove the beginObject() from my block of code, but the exception is still thrown.

Any suggestion on how to avoid this Nesting problem?

EDIT
Oh nevermind! I think it was some cache issue with the build: after I ran it again without the beginObject() in my block, it properly serialized it! I’ll test some more and report back!

1 Like

I’ll test some more and report back!

:+1:

I’m not sure how to get a proper instance of the JsonWriter

You can look into buildJsonString which is a small helper for this. Your code looks good otherwise. Just make sure to call beginObject/endObject or .obj() but not both at the same time (.obj() is basically doing the beginObject/endObject)

1 Like

Everything seems to work fine, thanks again!

1 Like

Nice! Thanks for the update :blue_heart:

1 Like

Hi @mbonnin, sorry to bother you again but I’m now trying to deserialize a different data class and I’m getting the exception:

java.lang.IllegalStateException: BufferedSourceJsonReader cannot rewind.
	at com.apollographql.apollo3.api.json.BufferedSourceJsonReader.rewind(BufferedSourceJsonReader.kt:939)
	at com.veeva.link.core.network.graphql.adapter.ObjectiveQuery_ResponseAdapter$Expert.fromJson(ObjectiveQuery_ResponseAdapter.kt:372)
	at com.veeva.link.core.network.graphql.adapter.ObjectiveQuery_ResponseAdapter$Expert.fromJson(ObjectiveQuery_ResponseAdapter.kt:352)
	at com.apollographql.apollo3.api.ObjectAdapter.fromJson(Adapters.kt:227)
	at com.apollographql.apollo3.api.ListAdapter.fromJson(Adapters.kt:30)

This is how I’m creating the JsonReader:

 ObjectiveQuery_ResponseAdapter.Expert.obj().list().fromJson(
                Buffer().writeUtf8(this).jsonReader(),
                CustomScalarAdapters.Empty,
            )

Since the adapter works when apollo uses it probably I’m using the wrong JsonReader. Any idea of which one should I use to support rewind()?

This happens if you have merged fields in your query. In that case, you can call .buffer() on JsonReader:

ObjectiveQuery_ResponseAdapter.Expert.obj().list().fromJson(
                Buffer().writeUtf8(this).jsonReader().buffer(),
                CustomScalarAdapters.Empty,
            )

This will make another copy of the whole Json in RAM so it’s preferrable to avoid it when possible but sometimes it’s not.

1 Like

Hey @mbonnin, thanks again for the quick answer! Meanwhile I went deep in the code of the adapter and I saw that I could just set the parameter buffered to true in the obj() extension, this should do the same thing that you suggested :slight_smile:

This is how my code looks now that is working:

            ObjectiveQuery_ResponseAdapter.Expert.obj(true).list().nullable().fromJson(
                Buffer().writeUtf8(this).jsonReader(),
                CustomScalarAdapters.Empty,
            )

Btw you guys have done a really good job with this library! Thanks!

Right :+1: This works too :slight_smile: Thanks for the follow up!

1 Like