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

Support Kotlin Serialization to enable multiplatform DSL Client #7

Closed
ermadmi78 opened this issue May 23, 2021 · 1 comment
Closed
Assignees
Labels
enhancement New feature or request
Milestone

Comments

@ermadmi78
Copy link
Owner

ermadmi78 commented May 23, 2021

Kobby supports generation of Jackson annotations for DTO classes to provide serialization / deserialization feature. But Jackson does not support Kotlin multiplatform serialization / deserialization. It makes impossible to use Kobby as multiplatform client. We have to support of Kotlinx Serialization for generated DSL client to use Kobby in multiplatform projects.

Examples

Gradle example
Maven example

Requirements

  • Gradle at least version 8.0 is required.
  • Maven at least version 3.9.1 is required.
  • Kotlin at least version 1.8.0 is required.
  • Kotlinx Serialization at least 1.5.0 is required.
  • Ktor at least version 2.0.0 is required for default adapters.

Implicit setup

To enable Kotlinx Serialization support just add org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0 dependency and configure serialization plugin.

Gradle

plugins {
    kotlin("jvm") version "1.8.20"
    kotlin("plugin.serialization") version "1.8.20"
    id("io.github.ermadmi78.kobby") version "3.0.0-beta.01"
}

dependencies {
    // Add this dependency to enable Kotlinx Serialization
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
}

Maven

<build>
    <plugins>
        <plugin>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-plugin</artifactId>
            <version>1.8.20</version>

            <dependencies>
                <dependency>
                    <groupId>org.jetbrains.kotlin</groupId>
                    <artifactId>kotlin-maven-serialization</artifactId>
                    <version>1.8.20</version>
                </dependency>
            </dependencies>

            <configuration>
                <compilerPlugins>
                    <plugin>kotlinx-serialization</plugin>
                </compilerPlugins>
            </configuration>

            <executions>
                <execution>
                    <id>compile</id>
                    <phase>compile</phase>
                    <goals>
                        <goal>compile</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>io.github.ermadmi78</groupId>
            <artifactId>kobby-maven-plugin</artifactId>
            <version>3.0.0-beta.01</version>
            <executions>
                <execution>
                    <phase>generate-sources</phase>
                    <goals>
                        <goal>generate-kotlin</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>

    <dependency>
        <groupId>org.jetbrains.kotlinx</groupId>
        <artifactId>kotlinx-serialization-json</artifactId>
        <version>1.5.0</version>
    </dependency>
</build>

Explicit setup

You can explicitly enable (or disable) Kotlinx Serialization support in the generated code, but you still need to add kotlinx-serialization-json dependency and configure serialization plugin. In addition to the "implicit setup" you can add:

Gradle

kobby {
    kotlin {
        dto {
            serialization {
                // Is Kotlinx Serialization enabled.
                // By default, "true" if "org.jetbrains.kotlinx:kotlinx-serialization-json" artifact
                // is in the project dependencies.
                enabled = true

                // Name of the class descriptor property for polymorphic serialization.
                classDiscriminator = "__typename"

                // Specifies whether encounters of unknown properties in the input JSON
                // should be ignored instead of throwing SerializationException.
                ignoreUnknownKeys = true

                // Specifies whether default values of Kotlin properties should be encoded to JSON.
                encodeDefaults = false
                
                // Specifies whether resulting JSON should be pretty-printed.
                prettyPrint = false
            }
        }
    }
}

Maven

<plugin>
    <groupId>io.github.ermadmi78</groupId>
    <artifactId>kobby-maven-plugin</artifactId>
    <version>3.0.0-beta.01</version>
    <executions>
        <execution>
            <phase>generate-sources</phase>
            <goals>
                <goal>generate-kotlin</goal>
            </goals>
            <configuration>
                <kotlin>
                    <dto>
                        <serialization>
                            <!-- Is Kotlinx Serialization enabled. -->
                            <!-- By default, "true" if "org.jetbrains.kotlinx:kotlinx-serialization-json" -->
                            <!-- artifact is in the project dependencies. -->
                            <enabled>true</enabled>

                            <!-- Name of the class descriptor property for polymorphic serialization. -->
                            <classDiscriminator>__typename</classDiscriminator>

                            <!-- Specifies whether encounters of unknown properties in the input JSON -->
                            <!-- should be ignored instead of throwing SerializationException. -->
                            <ignoreUnknownKeys>true</ignoreUnknownKeys>

                            <!-- Specifies whether default values of Kotlin properties -->
                            <!-- should be encoded to JSON. -->
                            <encodeDefaults>false</encodeDefaults>

                            <!-- Specifies whether resulting JSON should be pretty-printed. -->
                            <prettyPrint>false</prettyPrint>
                        </serialization>
                    </dto>
                </kotlin>
            </configuration>
        </execution>
    </executions>
</plugin>

Kotlinx Serialization entry point

The Kotlinx Serialization entry point in the generated DSL is placed near the DSL context entry point (root file xxx.kt, where xxx is the name of the context). For example, for a context named cinema it would look like this:

cinema.kt

/**
 * Default entry point to work with JSON serialization.
 */
public val cinemaJson: Json = Json {
  classDiscriminator = "__typename"
  ignoreUnknownKeys = true
  encodeDefaults = false
  prettyPrint = false
  serializersModule = SerializersModule {
    polymorphic(EntityDto::class) {
      subclass(FilmDto::class)
      subclass(ActorDto::class)
      subclass(CountryDto::class)
    }
    polymorphic(TaggableDto::class) {
      subclass(FilmDto::class)
      subclass(ActorDto::class)
    }
    polymorphic(NativeDto::class) {
      subclass(FilmDto::class)
      subclass(ActorDto::class)
    }
  }
}


public fun cinemaContextOf(adapter: CinemaAdapter): CinemaContext = CinemaContextImpl(adapter)

You must pass cinemaJson to the default adapters to ensure Kotlinx Serialization.

Simple Adapter configuration

val client = HttpClient(CIO) {
    install(ContentNegotiation) {
        json(cinemaJson)
    }
}

val context = cinemaContextOf(
    CinemaSimpleKtorAdapter(client, "http://localhost:8080/graphql")
)

Composite Adapter configuration

val client = HttpClient(CIO) {
    install(WebSockets)
}

val context = cinemaContextOf(
    CinemaCompositeKtorAdapter(
        client,
        "http://localhost:8080/graphql",
        "ws://localhost:8080/subscriptions"
    )
)

You don't need to pass cinemaJson to the composite adapter as it is configured as the default value of mapper argument:

public open class CinemaCompositeKtorAdapter(
  protected val client: HttpClient,
  protected val httpUrl: String,
  protected val webSocketUrl: String,
  protected val mapper: Json = cinemaJson, // cinemaJson is configured by default!
  protected val requestHeaders: suspend () -> Map<String, String> = { mapOf<String, String>() },
  protected val subscriptionPayload: suspend () -> JsonObject? = { null },
  protected val subscriptionReceiveTimeoutMillis: Long? = null,
  protected val httpAuthorizationTokenHeader: String = "Authorization",
  protected val webSocketAuthorizationTokenHeader: String = "authToken",
  protected val idGenerator: () -> String = { Random.nextLong().toString() },
  protected val listener: (CinemaRequest) -> Unit = {},
) : CinemaAdapter {
  // Skipped
}

Custom serializers

You can configure a custom serializer to any type associated with a scalar. For example, let's define a Date scalar in our schema and associate it with a java.time.LocalDate.

scalar Date

type Query {
    extract: Date!
}

First, we must write custom serializer for java.time.LocalDate:

object LocalDateSerializer : KSerializer<LocalDate> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: LocalDate) =
        encoder.encodeString(value.toString())

    override fun deserialize(decoder: Decoder): LocalDate =
        LocalDate.parse(decoder.decodeString())
}

Second, we must bind java.time.LocalDate to Date scalar and set up LocalDateSerializer for it:

Gradle (see scalar mapping)

kobby {
    kotlin {
        scalars = mapOf(
            "Date" to typeOf("java.time", "LocalDate")
                .serializer(
                    "io.github.ermadmi78.kobby.cinema.api.kobby.kotlin.dto", // package name
                    "LocalDateSerializer" // class name
                )
        )
    }
}

Maven (see scalar mapping)

<plugin>
    <groupId>io.github.ermadmi78</groupId>
    <artifactId>kobby-maven-plugin</artifactId>
    <version>3.0.0-beta.01</version>
    <executions>
        <execution>
            <phase>generate-sources</phase>
            <goals>
                <goal>generate-kotlin</goal>
            </goals>
            <configuration>
                <kotlin>
                    <scalars>
                        <Date>
                            <packageName>java.time</packageName>
                            <className>LocalDate</className>
                            <serializer>
                                <packageName>
                                    io.github.ermadmi78.kobby.cinema.api.kobby.kotlin.dto
                                </packageName>
                                <className>LocalDateSerializer</className>
                            </serializer>
                        </Date>
                    </scalars>
                </kotlin>
            </configuration>
        </execution>
    </executions>
</plugin>

Mixing serialization engines

During the development process, it turned out that the Kotlinx Serialization engine does not like type Any at all.
To get around this limitation, the plugin replaces type Any in the generated code according to the following rules:

For example, the GraphQL request DTO for Jackson serialization engine looks like this:

public data class CinemaRequest(
  public val query: String,

  @JsonInclude(value = JsonInclude.Include.NON_EMPTY)
  public val variables: Map<String, Any?>? = null,

  @JsonInclude(value = JsonInclude.Include.NON_ABSENT)
  public val operationName: String? = null,
)

But the same DTO for Kotlinx Serialization engine looks like this:

@Serializable
public data class CinemaRequest(
  public val query: String,
  public val variables: JsonObject? = null,
  public val operationName: String? = null,
)

Such a replacement leads to the fact that it is impossible to generate DTO classes that can be serialized using Jackson and Kotlinx Serialization at the same time. Therefore, you will need to choose one of the serialization engines and use only that.

@ermadmi78 ermadmi78 added the enhancement New feature or request label May 23, 2021
@ermadmi78 ermadmi78 self-assigned this May 23, 2021
@ermadmi78 ermadmi78 added this to the Version 2.0 milestone Feb 14, 2022
@ermadmi78 ermadmi78 removed this from the Version 2.0 milestone Oct 7, 2022
@ermadmi78 ermadmi78 added this to the Version 3.0 milestone Feb 13, 2023
@ermadmi78 ermadmi78 closed this as not planned Won't fix, can't repro, duplicate, stale Feb 22, 2023
@ermadmi78 ermadmi78 reopened this Feb 26, 2023
@ermadmi78
Copy link
Owner Author

Available since release 3.0.0-beta.01

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

When branches are created from issues, their pull requests are automatically linked.

1 participant