Тип-Безопасность с необязательными полями в классе данных для JSON

Я создаю 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 не имеет значения NULL. По-моему, я мог бы достичь этого с помощью дженериков, но это будет очень многословным, как только будет задействовано несколько необязательных полей.

Меня интересуют любые предложения.

В соответствии с запросом я добавлю еще немного кода, чтобы показать, что мне нужно.

 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 

Пара вариантов:

Для необязательных значений вы можете использовать свойство non-nullable со значением по умолчанию, как в

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

Я думаю, что вы можете добиться, создав несколько конструкторов для класса данных согласно упоминанию в документации класса

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

Один из возможных способов сделать это – использовать закрытые классы :

 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 } 

Оба закрытых класса и интерфейсов позволяют выполнить проверку времени компиляции, когда вы не получаете доступ к aboutMe свойству aboutMe . Однако, в зависимости от расширения вашего API, вам может быть лучше с интерфейсами, которые упрощают компоновку. Доступ к двум методам можно легко получить через выражение when, когда вы получаете общий / родительский параметр в функции.