-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support for Kotlin Serialization (#327)
Introduces support for [kotlinx.serialization](https://kotlinlang.org/docs/serialization.html) (KxS) annotations on the generated models. A new `--serialization-library` option allows the user to choose between `JACKSON` and `KOTLINX_SERIALIZATION`. The Jackson implementation is the default and has not changed, and users will get the same output as previously. For date and datetime [kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) is used to make the generated code work in multi platform / non-JVM environments. Date is translated to `kotlinx.datetime.LocalDate` and datetime to `kotlinx.datetime.Instant`. **Unsupported features** 1. When KxS is chosen `additionalProperties: true` is not supported. This is due to fact that supporting a `map<String,Any?>` is quite tricky (if at all possible?) with Kotlinx.Serialization, whereas Jackson has `JsonAnyGetter`/`JsonAnySetter` to support this. We might be able to configure the property with [@contextual](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-contextual/) to instruct the plugin to resolve serializer at runtime. Related: Kotlin/kotlinx.serialization#1978. 3. URI and UUID are translated to strings as no native Kotlin types exist ([yet](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.uuid/-experimental-uuid-api/)?). 4. `number` without format is currently translated to Java's `BigDecimal` which will not work in non-JVM Kotlin. Works fine for `number`/`float` and `number`/`double` as these are translated into Kotlin types. **Tests** I am unsure how many more tests for the examples we want. Many of the examples are concerned with translating OpenAPI into models, and are not specific to the serialization annotations. Any opinions/input/suggestion are welcome.
- Loading branch information
1 parent
741451e
commit 22944d3
Showing
32 changed files
with
886 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
val fabrikt: Configuration by configurations.creating | ||
|
||
val generationDir = "$buildDir/generated" | ||
val apiFile = "$projectDir/openapi/api.yaml" | ||
|
||
sourceSets { | ||
main { java.srcDirs("$generationDir/src/main/kotlin") } | ||
test { java.srcDirs("$generationDir/src/test/kotlin") } | ||
} | ||
|
||
plugins { | ||
id("org.jetbrains.kotlin.jvm") version "1.8.20" // Apply the Kotlin JVM plugin to add support for Kotlin. | ||
kotlin("plugin.serialization") version "1.8.20" | ||
} | ||
|
||
java { | ||
sourceCompatibility = JavaVersion.VERSION_17 | ||
targetCompatibility = JavaVersion.VERSION_17 | ||
} | ||
|
||
val junitVersion: String by rootProject.extra | ||
val kotlinxSerializationVersion: String by rootProject.extra | ||
val kotlinxDateTimeVersion: String by rootProject.extra | ||
|
||
dependencies { | ||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion") | ||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDateTimeVersion") | ||
|
||
testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion") | ||
testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion") | ||
testImplementation("org.junit.jupiter:junit-jupiter-params:$junitVersion") | ||
testImplementation("org.assertj:assertj-core:3.24.2") | ||
} | ||
|
||
tasks { | ||
|
||
val generateCode by creating(JavaExec::class) { | ||
inputs.files(apiFile) | ||
outputs.dir(generationDir) | ||
outputs.cacheIf { true } | ||
classpath = rootProject.files("./build/libs/fabrikt-${rootProject.version}.jar") | ||
mainClass.set("com.cjbooms.fabrikt.cli.CodeGen") | ||
args = listOf( | ||
"--output-directory", generationDir, | ||
"--base-package", "com.example", | ||
"--api-file", apiFile, | ||
"--validation-library", "NO_VALIDATION", | ||
"--targets", "http_models", | ||
"--serialization-library", "KOTLINX_SERIALIZATION", | ||
"--http-model-opts", "SEALED_INTERFACES_FOR_ONE_OF", | ||
) | ||
dependsOn(":jar") | ||
dependsOn(":shadowJar") | ||
} | ||
|
||
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> { | ||
kotlinOptions.jvmTarget = "17" | ||
dependsOn(generateCode) | ||
} | ||
|
||
|
||
withType<Test> { | ||
useJUnitPlatform() | ||
jvmArgs = listOf("--add-opens=java.base/java.lang=ALL-UNNAMED", "--add-opens=java.base/java.util=ALL-UNNAMED") | ||
|
||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
openapi: "3.0.0" | ||
info: | ||
version: 1.0.0 | ||
title: Swagger Petstore | ||
license: | ||
name: MIT | ||
servers: | ||
- url: http://petstore.swagger.io/v1 | ||
paths: {} | ||
components: | ||
schemas: | ||
TransportationDevice: | ||
type: object | ||
required: | ||
- deviceType | ||
- make | ||
- model | ||
properties: | ||
deviceType: | ||
type: string | ||
enum: | ||
- bike | ||
- skateboard | ||
- rollerskates | ||
- Ho_ver-boaRD | ||
make: | ||
type: string | ||
model: | ||
type: string | ||
format: uuid | ||
Pet: | ||
type: object | ||
required: | ||
- id | ||
- name | ||
- dateOfBirth | ||
properties: | ||
id: | ||
type: integer | ||
format: int64 | ||
name: | ||
type: string | ||
tag: | ||
type: string | ||
dateOfBirth: | ||
type: string | ||
format: date | ||
lastFedAt: | ||
type: string | ||
format: date-time | ||
earTagUuid: | ||
type: string | ||
format: uuid | ||
imageUrl: | ||
type: string | ||
format: uri | ||
Pets: | ||
type: array | ||
maxItems: 100 | ||
items: | ||
$ref: "#/components/schemas/Pet" | ||
Phone: | ||
oneOf: | ||
- $ref: "#/components/schemas/LandlinePhone" | ||
- $ref: "#/components/schemas/MobilePhone" | ||
discriminator: | ||
propertyName: type | ||
mapping: | ||
landline: '#/components/schemas/LandlinePhone' | ||
mobile: '#/components/schemas/MobilePhone' | ||
LandlinePhone: | ||
type: object | ||
required: | ||
- number | ||
- area_code | ||
properties: | ||
number: | ||
type: string | ||
area_code: | ||
type: string | ||
MobilePhone: | ||
type: object | ||
required: | ||
- number | ||
properties: | ||
number: | ||
type: string | ||
Error: | ||
type: object | ||
required: | ||
- code | ||
- message | ||
properties: | ||
code: | ||
type: integer | ||
format: int32 | ||
message: | ||
type: string |
91 changes: 91 additions & 0 deletions
91
...otlinx/src/test/kotlin/com/cjbooms/fabrikt/models/kotlinx/KotlinxSerializationEnumTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
package com.cjbooms.fabrikt.models.kotlinx | ||
|
||
import com.example.models.TransportationDevice | ||
import com.example.models.TransportationDeviceDeviceType | ||
import kotlinx.serialization.SerializationException | ||
import kotlinx.serialization.encodeToString | ||
import kotlinx.serialization.json.Json | ||
import org.assertj.core.api.Assertions.assertThat | ||
import org.junit.jupiter.api.Test | ||
import org.junit.jupiter.api.assertThrows | ||
|
||
class KotlinxSerializationEnumTest { | ||
|
||
@Test | ||
fun `must serialize entity with enum field`() { | ||
val device = TransportationDevice( | ||
deviceType = TransportationDeviceDeviceType.BIKE, | ||
make = "Specialized", | ||
model = "Chisel" | ||
) | ||
val json = Json.encodeToString(device) | ||
assertThat(json).isEqualTo(""" | ||
{"deviceType":"bike","make":"Specialized","model":"Chisel"} | ||
""".trimIndent()) | ||
} | ||
|
||
@Test | ||
fun `must deserialize entity with enum field`() { | ||
val json = """ | ||
{"deviceType":"bike","make":"Specialized","model":"Chisel"} | ||
""".trimIndent() | ||
val device = Json.decodeFromString(TransportationDevice.serializer(), json) | ||
assertThat(device).isEqualTo( | ||
TransportationDevice( | ||
deviceType = TransportationDeviceDeviceType.BIKE, | ||
make = "Specialized", | ||
model = "Chisel" | ||
) | ||
) | ||
} | ||
|
||
@Test | ||
fun `must fail with SerializationException if enum value is not valid`() { | ||
val json = """ | ||
{"deviceType":"car","make":"Specialized","model":"Chisel"} | ||
""".trimIndent() | ||
val exception = assertThrows<SerializationException> { | ||
Json.decodeFromString<TransportationDevice>(json) | ||
} | ||
assertThat(exception.message).isEqualTo("com.example.models.TransportationDeviceDeviceType does not contain element with name 'car' at path \$.deviceType") | ||
} | ||
|
||
@Test | ||
fun `must fail with SerializationException if required fields are missing`() { | ||
val json = """ | ||
{"deviceType":"bike"} | ||
""".trimIndent() | ||
val exception = assertThrows<SerializationException> { | ||
Json.decodeFromString<TransportationDevice>(json) | ||
} | ||
assertThat(exception.message).contains("Fields [make, model] are required for type with serial name 'com.example.models.TransportationDevice', but they were missing at path: \$") | ||
} | ||
|
||
@Test | ||
fun `must serialize entity with enum field with mixed case`() { | ||
val device = TransportationDevice( | ||
deviceType = TransportationDeviceDeviceType.HO_VER_BOA_RD, | ||
make = "Hover", | ||
model = "Board" | ||
) | ||
val json = Json.encodeToString(device) | ||
assertThat(json).isEqualTo(""" | ||
{"deviceType":"Ho_ver-boaRD","make":"Hover","model":"Board"} | ||
""".trimIndent()) | ||
} | ||
|
||
@Test | ||
fun `must deserialize entity with enum field with mixed case`() { | ||
val json = """ | ||
{"deviceType":"Ho_ver-boaRD","make":"Hover","model":"Board"} | ||
""".trimIndent() | ||
val device = Json.decodeFromString(TransportationDevice.serializer(), json) | ||
assertThat(device).isEqualTo( | ||
TransportationDevice( | ||
deviceType = TransportationDeviceDeviceType.HO_VER_BOA_RD, | ||
make = "Hover", | ||
model = "Board" | ||
) | ||
) | ||
} | ||
} |
52 changes: 52 additions & 0 deletions
52
...est/kotlin/com/cjbooms/fabrikt/models/kotlinx/KotlinxSerializationOneOfPolymorphicTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package com.cjbooms.fabrikt.models.kotlinx | ||
|
||
import com.example.models.LandlinePhone | ||
import com.example.models.Phone | ||
import kotlinx.serialization.encodeToString | ||
import org.assertj.core.api.Assertions.assertThat | ||
import org.junit.jupiter.api.Test | ||
|
||
class KotlinxSerializationOneOfPolymorphicTest { | ||
|
||
@Test | ||
fun `must serialize Phone with type info`() { | ||
val phone: Phone = LandlinePhone(number = "1234567890", areaCode = "123") | ||
val json = kotlinx.serialization.json.Json.encodeToString(phone) | ||
|
||
// Note that "type" is added because we are serializing a subtype of Phone | ||
// (See https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#sealed-classes) | ||
assertThat(json).isEqualTo(""" | ||
{"type":"landline","number":"1234567890","area_code":"123"} | ||
""".trimIndent()) | ||
} | ||
|
||
@Test | ||
fun `must serialize LandlinePhone without type info`() { | ||
val phone: LandlinePhone = LandlinePhone(number = "1234567890", areaCode = "123") | ||
val json = kotlinx.serialization.json.Json.encodeToString(phone) | ||
|
||
// Note that "type" is not added because we are serializing the specific class LandlinePhone | ||
// (See https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#sealed-classes) | ||
assertThat(json).isEqualTo(""" | ||
{"number":"1234567890","area_code":"123"} | ||
""".trimIndent()) | ||
} | ||
|
||
@Test | ||
fun `must deserialize Phone into LandlinePhone`() { | ||
val json = """ | ||
{"type":"landline","number":"1234567890","area_code":"123"} | ||
""".trimIndent() | ||
val phone: Phone = kotlinx.serialization.json.Json.decodeFromString(json) | ||
assertThat(phone).isEqualTo(LandlinePhone(number = "1234567890", areaCode = "123")) | ||
} | ||
|
||
@Test | ||
fun `must deserialize LandlinePhone specific class`() { | ||
val json = """ | ||
{"number":"1234567890","area_code":"123"} | ||
""".trimIndent() | ||
val phone: LandlinePhone = kotlinx.serialization.json.Json.decodeFromString(json) | ||
assertThat(phone).isEqualTo(LandlinePhone(number = "1234567890", areaCode = "123")) | ||
} | ||
} |
Oops, something went wrong.