Kotlin- Ktor -如何处理PATCH调用中的可选API资源字段?

pgvzfuti  于 2023-06-24  发布在  Kotlin
关注(0)|答案(4)|浏览(168)

当使用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"

你想用补丁删除这个国家:

"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<String>?,但它抱怨缺少Optional的序列化,最好我不想让所有数据都成为var的Optional。
注意:我正在寻找一个更结构化的解决方案,也与所有其他资源的API(10+类)。

eh57zj3b

eh57zj3b1#

第二个解决方案,基于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
5ssjco0h

5ssjco0h2#

您可以使用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) }
        }
    }
}
nbysray5

nbysray53#

在www.example.com的帮助下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
mwkjh3gx

mwkjh3gx4#

第三种解决方案基于Etienne。它修复了解码器不能被反序列化为T并抛出的问题。基础版本Map为“不存在”。
还重新命名了病例。

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

@Serializable(with = AbsentOrNullOrValueSerializer::class)
sealed interface AbsentOrNullOrValue<out T>

object Absent : AbsentOrNullOrValue<Nothing> {
    override fun toString(): String = "Absent"
}

object NullValue : AbsentOrNullOrValue<Nothing> {
    override fun toString(): String = "Null"
}

class WithValue<T>(val value: T) : AbsentOrNullOrValue<T> {
    override fun toString(): String = value.toString()
}

open class AbsentOrNullOrValueSerializer<T>(private val valueSerializer: KSerializer<T>) :
    KSerializer<AbsentOrNullOrValue<T>> {

    override val descriptor: SerialDescriptor = valueSerializer.descriptor

    @OptIn(ExperimentalSerializationApi::class)
    override fun deserialize(decoder: Decoder): AbsentOrNullOrValue<T> {
        return if (!decoder.decodeNotNullMark()) {
            NullValue
        } else {
            WithValue(valueSerializer.deserialize(decoder))
        }
    }

    @OptIn(ExperimentalSerializationApi::class)
    override fun serialize(encoder: Encoder, value: AbsentOrNullOrValue<T>) {
        when (value) {
            is Absent -> {}
            is NullValue -> {
                encoder.encodeNull()
            }
            is WithValue -> valueSerializer.serialize(encoder, value.value)
        }
    }
}

相关问题