【问题标题】:Kotlin - Ktor - How to handle Optional API resource fields in PATCH calls?Kotlin - Ktor - 如何处理 PATCH 调用中的可选 API 资源字段?
【发布时间】:2022-07-06 20:14:57
【问题描述】:

当使用 Ktor(和 Kotlin)实现 REST API 时,它支持 Kotlin 的可选字段处理。哪个适用于 POST 和 GET,但是 PATCH(更新)呢?

例如,您有以下资源:

@Serializable
data class MyAddress(
    var line1: String? = null,
    var line2: String? = null,
    var city: String? = null,
    var postal_code: String? = null,
    var state: String? = null,
    var country: String? = null
)

所以所有 MyAddress 字段都是可选的(具有默认值)。

当您使用 POST 创建地址时:

   \"line1\" : \"line1\",
   \"country\" : \"XX\"

并且您不想使用 PATCH 删除国家/地区:

   \"country\" : null

资源的最终结果应该是:

   \"line1\" : \"line1\"

但是如何通过解析 PATCH 请求的 json 来确定呢?因为据我所知,没有办法确定它是默认为null,还是已提交。

此外,MyAddress 的默认 null 值是必需的,否则解析将不起作用。

代码示例:

import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

@kotlinx.serialization.Serializable
data class MyAddress(
    var line1: String? = null,
    var line2: String? = null,
    var city: String? = null,
    var postal_code: String? = null,
    var state: String? = null,
    var country: String? = null
)

fun main() {
    val jsonStringPOST = \"{\\\"line1\\\":\\\"street\\\",\\\"country\\\":\\\"GB\\\"}\"
    println(\"JSON string is: $jsonStringPOST\")

    val myAddressPost = Json.decodeFromString<MyAddress>(jsonStringPOST)
    println(\"MyAddress object: $myAddressPost\")

    val jsonStringPATCH = \"{\\\"country\\\":null}\"
    println(\"JSON string is: $jsonStringPATCH\")

    val myAddressPatch = Json.decodeFromString<MyAddress>(jsonStringPATCH)
    println(\"MyAddress object: $myAddressPatch\")
}

我也尝试添加Optional&lt;String&gt;?,但它抱怨缺少Optional 的序列化,最好我不想将所有数据都设为var\'s Optionals。

注意:我正在寻找一个更结构化的解决方案,它也适用于 api 中的所有其他资源(10 多个类)。

  • 对于 UPDATE 请求,我会收到 JsonObject 并遍历所有已知键以确定如何更新它们。
  • 感谢您的回复。这确实是一种选择,但是当存在分层结构时(例如,将 MyAddress 作为 Person 的值,而 Person 是 Group 的值),您必须“知道”所有键的名称。我认为这不是真正可扩展的。
  • 然后我在这里看到两个选项。要么让所有属性为空但没有默认值,并强制客户端每次都发送所有键,要么只使用 JSON 元素对象始终如一地工作。
  • 第一个选项不会真正起作用,因为从用户/API 的角度来看,这不是 REST 应该如何工作的。我希望有一个更有条理的解决方案。就像 Kotlin 有(它已被删除)一个 Optional 注释一样。类似的东西,具有与 Java Optional 相同的“功能”;您可以确定 1) 发送时带有值,2) 发送时带有 null 值,3) 未发送

标签: api rest kotlin optional ktor


【解决方案1】:

您可以将sealed interface 用于地址的一部分,以表示未定义值、缺少值和实际值。对于此接口,您需要编写一个序列化程序,它将根据您的逻辑对值进行编码和解码。我不擅长kotlinx.serialization 框架,所以我写了一个尽可能简单的例子。

import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.request.*
import io.ktor.server.routing.*
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

fun main() {
    embeddedServer(Netty, port = 4444) {
        install(ContentNegotiation) {
            json()
        }

        routing {
            post {
                val address = call.receive<Address>()
                println(address)
            }
        }
    }.start()
}

@Serializable
data class Address(val line1: MyValue = Undefined, val country: MyValue = Undefined)

@Serializable(with = AddressValueSerializer::class)
sealed interface MyValue
object Undefined: MyValue {
    override fun toString(): String = "Undefined"
}
object Absent: MyValue {
    override fun toString(): String = "Absent"
}
class WithValue(val value: String): MyValue {
    override fun toString(): String = value
}

object AddressValueSerializer: KSerializer<MyValue> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("AddressValue", PrimitiveKind.STRING)

    override fun deserialize(decoder: Decoder): MyValue {
        return try {
            WithValue(decoder.decodeString())
        } catch (cause: SerializationException) {
            Absent
        }
    }

    @OptIn(ExperimentalSerializationApi::class)
    override fun serialize(encoder: Encoder, value: MyValue) {
        when (value) {
            is Undefined -> {}
            is Absent -> { encoder.encodeNull() }
            is WithValue -> { encoder.encodeString(value.value) }
        }
    }
}

【讨论】:

  • 谢谢阿列克谢!在 medium.com 的帮助下,我找到了类似的解决方案
【解决方案2】:

在 medium.com 的帮助下,我找到了以下解决方案:


@Serializable(with = OptionalPropertySerializer::class)
open class OptionalProperty<out T> {

    object NotPresent : OptionalProperty<Nothing>()

    data class Present<T>(val value: T) : OptionalProperty<T>() {
        override fun toString(): String {
            return value.toString()
        }
    }

    fun isPresent() : Boolean {
        return this is Present
    }
    fun isNotPresent(): Boolean {
        return this is NotPresent
    }

    fun isEmpty(): Boolean {
        return (this is Present) && this.value == null
    }
    fun hasValue(): Boolean {
        return (this is Present) && this.value != null
    }

    override fun toString(): String {
        if(this is NotPresent) {
            return "<NotPresent>"
        }
        return super.toString()
    }
}

open class OptionalPropertySerializer<T>(private val valueSerializer: KSerializer<T>) : KSerializer<OptionalProperty<T>> {
     override val descriptor: SerialDescriptor = valueSerializer.descriptor

     override fun deserialize(decoder: Decoder): OptionalProperty<T> =
        OptionalProperty.Present(valueSerializer.deserialize(decoder))

     override fun serialize(encoder: Encoder, value: OptionalProperty<T>) {
        when (value) {
            is OptionalProperty.NotPresent -> {}
            is OptionalProperty.Present -> valueSerializer.serialize(encoder, value.value)
        }
    }
}

@Serializable
private data class MyAddressNew(
    var line1: OptionalProperty<String?> = OptionalProperty.NotPresent,
    var line2: OptionalProperty<String?> = OptionalProperty.NotPresent,
    var city: OptionalProperty<String?> = OptionalProperty.NotPresent,
    var postal_code: OptionalProperty<String?> = OptionalProperty.NotPresent,
    var state: OptionalProperty<String?> = OptionalProperty.NotPresent,
    var country: OptionalProperty<String?> = OptionalProperty.NotPresent,
)


fun main() {
    val jsonStringPOST = "{\"line1\":\"street\",\"country\":\"GB\"}"
    println("JSON string is: $jsonStringPOST")

    val myAddressPost = Json.decodeFromString<MyAddressNew>(jsonStringPOST)
    println("MyAddress object: $myAddressPost")

    val jsonStringUPDATE = "{\"country\":null}"
    println("JSON string is: $jsonStringUPDATE")

    val myAddressUpdate = Json.decodeFromString<MyAddressNew>(jsonStringUPDATE)
    println("MyAddress object: $myAddressUpdate")
    if(myAddressUpdate.country.isPresent()) {
        println("Update country: ${myAddressUpdate.country}")
    } else {
        println("No update for country: ${myAddressUpdate.country}")
    }
}

这打印:

JSON string is: {"line1":"street","country":"GB"}
MyAddress object: MyAddressNew(line1=street, line2=<NotPresent>, city=<NotPresent>, postal_code=<NotPresent>, state=<NotPresent>, country=GB)
JSON string is: {"country":null}
MyAddress object: MyAddressNew(line1=<NotPresent>, line2=<NotPresent>, city=<NotPresent>, postal_code=<NotPresent>, state=<NotPresent>, country=null)
Update country: null

【讨论】:

  • 我喜欢你序列化/反序列化泛型类型的值,但我不喜欢 OptionalProperty 对象的可为空值以及 OptionalProperty 类是打开的事实。此外,OptionalProperty&lt;Nothing&gt;() 看起来很奇怪。
  • 我将它改写为sealed interface 而不是open class,但不确定sealed interface 是否可以完成我的应用程序。您有什么建议可以替代Nothing 来解决通用值?
  • 如果您有一个密封的接口,那么您可以像我在示例中所做的那样用对象表示缺少值和未定义的值。
  • 在您的示例中它确实有效,但如果您添加泛型,它不会。您必须为所有 3 个选项传递一些内容,例如:sealed interface OptionalValue&lt;out T&gt;object Undefined: OptionalValue&lt;Nothing&gt;{ .. } object Absent: OptionalValue&lt;Nothing&gt;{ .. } 否则序列化程序无法编译...
  • 是的。当我尝试使用泛型时,我传递了Any 值,因为任何值都可以是未定义的或不存在的。
【解决方案3】:

第二种解决方案,基于 Aleksei 的示例:


@Serializable
data class Address2(val line1: OptionalValue<String> = Undefined, val country: OptionalValue<String> = Undefined)

@Serializable(with = OptionalValueSerializer::class)
sealed interface OptionalValue<out T>
object Undefined: OptionalValue<Nothing> {
    override fun toString(): String = "Undefined"
}
object Absent: OptionalValue<Nothing> {
    override fun toString(): String = "Absent"
}
class WithValue<T>(val value: T): OptionalValue<T> {
    override fun toString(): String = value.toString()
}

open class OptionalValueSerializer<T>(private val valueSerializer: KSerializer<T>) : KSerializer<OptionalValue<T>> {
    override val descriptor: SerialDescriptor = valueSerializer.descriptor

    override fun deserialize(decoder: Decoder): OptionalValue<T> {
        return try {
            WithValue(valueSerializer.deserialize(decoder))
        } catch (cause: SerializationException) {
            Absent
        }
    }

    override fun serialize(encoder: Encoder, value: OptionalValue<T>) {
        when (value) {
            is Undefined -> {}
            is Absent -> { encoder.encodeNull() }
            is WithValue -> valueSerializer.serialize(encoder, value.value)
        }
    }
}

fun main() {
    val jsonStringPOST = "{\"line1\":\"street\",\"country\":\"GB\"}"
    println("JSON string is: $jsonStringPOST")

    val myAddressPost = Json.decodeFromString<Address2>(jsonStringPOST)
    println("MyAddress object: $myAddressPost")

    val jsonStringUPDATE = "{\"country\":null}"
    println("JSON string is: $jsonStringUPDATE")

    val myAddressUpdate = Json.decodeFromString<Address2>(jsonStringUPDATE)
    println("MyAddress object: $myAddressUpdate")
    if(myAddressUpdate.country is Absent || myAddressUpdate.country is WithValue) {
        println("Update country: ${myAddressUpdate.country}")
    } else {
        println("No update for country: ${myAddressUpdate.country}")
    }
}

输出是:

JSON string is: {"line1":"street","country":"GB"}
MyAddress object: Address2(line1=street, country=GB)
JSON string is: {"country":null}
MyAddress object: Address2(line1=Undefined, country=Absent)
Update country: Absent

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-08-13
    • 2015-05-28
    • 2018-03-09
    • 2021-10-14
    • 1970-01-01
    • 2018-06-26
    • 1970-01-01
    • 2021-03-29
    相关资源
    最近更新 更多