Type-Safety,包含JSON数据类中的可选字段

时间:2017-10-01 13:56:32

标签: json rest kotlin

我正在Kotlin中构建一个REST-API,我正在尝试编写我的数据类,以便我也可以使用它们来访问API。 我打算有一个共同的“数据库”,它将使用API​​在服务器和客户端之间共享。

到目前为止这很有效,但现在我需要在JSON响应中建模可选字段。例如:我有一个User对象(可以通过/user/{id}端点以典型的REST方式访问)。现在,您并不总是需要例如用户的“关于我”文本,因此默认情况下它不包含在响应中。但是,如果您指定"aboutme"字段(/user/{id}?fields=aboutme),则该字段将包含在回复中。

我可以按如下方式对数据类进行建模:

data class User(id: UUID, name: String, aboutMe: String?)

但是现在每次访问它时我都必须对字段进行空检查,即使它显然不是空的。我想为API创建一个类型安全的前端,以便在我这样做时,myCoolApi.getUser({id}, User::aboutMe)我会得到一个User对象,其中aboutMe不可为空。我认为,我可以通过某种方式用泛型实现这一点,但只要涉及多个可选字段,就会变得非常冗长。

我对任何建议感兴趣。

根据要求,我会添加更多代码来展示我的目标。

class MyApi {

    fun getUser(id: UUID, vararg fields: KProperty1<User, *>): User

}

// usage:

val myApi: MyApi = TODO()
val userId: UUID = TODO()
val aboutMe: String = myApi.getUser(userId).aboutMe // does not compile, aboutMe field not specified so aboutMe is nullable
val aboutMe2: String = myApi.getUser(userId, User::aboutMe).aboutMe // compiles, about me field was specified and thus cannot be null

3 个答案:

答案 0 :(得分:0)

我认为您可以通过在Class

的文档中提及为数据类创建多个构造函数来实现
//data class User(val id: String, val name: String, val aboutMe: String? = null)

data class User(val id: String, val name: String) {
    constructor(id: String, name: String, aboutMe: String? = null) : this(id, name)
}

或者你可以使用@JvmOverloads根据传递的参数创建构造函数。您可以找到有关@JvmOverloads

的更多信息
data class Users @JvmOverloads constructor(val id: String, val name: String, val aboutMe: String? = null)

答案 1 :(得分:0)

尝试多种选择:

对于可选值,您可以使用具有默认值的非可空属性,如

data class User (val id: Long, val name: String, val aboutMe: String = "")

如果您可以放弃数据类,而是使用支持继承的常规类,则可以使用另一种替代方法。

open class User (val id: Long, val name: String, val aboutMe: String)

class User_  (id: Long, name: String, aboutMe: String?) 
       : User(id, name, aboutMe ?: "")

class MyCoolApi {

    fun getUser(id: Long): User {

        // do you db lookup or something like that
        // val name = ... from db
        // val aboutMe = ... from db
        return User_(id, name, aboutMe)
    }
}

答案 2 :(得分:0)

执行此操作的一种可能方法是使用sealed classes

sealed class ApiUser(val id: UUID, val name: String)

class ApiUserPlain(id: UUID, name: String) :
        ApiUser(id, name)

class ApiUserAbout(id: UUID, name: String, val aboutMe: String) :
        ApiUser(id, name)

fun getUser(userId: Long): ApiUserPlain {
    return ApiUserPlain(UUID.randomUUID(), userId.toString())
}

fun getUser(userId: Long, about: String): ApiUserAbout {
    return ApiUserAbout(UUID.randomUUID(), userId.toString(), about)
}

fun test() {
    val userId = 2L
    val aboutMe: String = getUser(userId).aboutMe // does not compile
    val aboutMe2: String = getUser(userId, "about").aboutMe // compiles
}

另一种方法是使用multiple interfaces

interface ApiUserPlain {
    val id: UUID
    val name: String
}

interface ApiUserAbout {
    val aboutMe: String
}

class PlainUser(
        override val id: UUID,
        override val name: String
) : ApiUserPlain

class AboutUser(
        override val id: UUID,
        override val name: String,
        override val aboutMe: String
) : ApiUserPlain, ApiUserAbout

fun getUser(userId: Long): PlainUser {
    return PlainUser(UUID.randomUUID(), userId.toString())
}

fun getUser(userId: Long, about: String): AboutUser {
    return AboutUser(UUID.randomUUID(), userId.toString(), about)
}

fun test() {
    val userId = 2L
    val aboutMe: String = getUser(userId).aboutMe // does not compile
    val aboutMe2: String = getUser(userId, "about").aboutMe // compiles
}

密封的类和接口都允许您完成编译时验证,即您没有访问不存在的aboutMe属性。但是,根据API的扩展,您可能会更好地使用允许更轻松组合的界面。当您在函数中获得泛型/父参数时,可以通过when expression轻松访问这两种方法。