在JSON数据类中带有可选字段的types安全

我正在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创建一个types安全的前端,所以当我这样做时,例如myCoolApi.getUser({id}, User::aboutMe)我将得到一个User对象,其中aboutMe不可为空。 我想可以用generics来实现这一点,但是如果涉及多个可选字段,那将会变得非常冗长。

我有兴趣的任何建议。

根据要求,我会添加更多的代码来显示我所追求的。

 class MyApi { fun getUser(id: UUID, vararg fields: KProperty1): 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 

几个选项来尝试:

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

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

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

 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) } } 

我想你可以通过在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来创建基于传递参数的构造函数。 你可以find更多关于@JvmOverloads的信息

 data class Users @JvmOverloads constructor(val id: String, val name: String, val aboutMe: String? = null) 

一个可能的方法是使用密封类 :

 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 } 

另一种方法是使用多个接口 :

 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 } 

密封的类和接口都允许您完成编译时validation,您不访问不存在的aboutMe属性。 然而,取决于你的API的扩展,你可能会更好的接口,更容易组成。 在函数中获取通用/父参数时,通过whenexpression式可以轻松访问这两种方法。