How to generate oneOf using springdoc from a Kotlin sealed class - kotlin

I have an existing Kotlin model which is a sealed class
sealed class AnyShape(val type: ShapeType) {
enum class ShapeType { Square, Circle }
data class Square(val size: Float) : AnyShape(ShapeType.Square)
data class Circle(val radius: Float) : AnyShape(ShapeType.Circle)
}
My existing, manually written OpenAPI spec defines this model using the oneOf to express the polymorphism:
components:
schemas:
AnyShape:
discriminator:
propertyName: type
oneOf:
- $ref: '#/components/schemas/Square'
- $ref: '#/components/schemas/Circle'
Circle:
required:
- radius
- type
type: object
properties:
radius:
type: number
format: float
Square:
required:
- size
- type
type: object
properties:
size:
type: number
format: float
Now, instead of maintaining the yaml spec manually, I'd like to generate it from the Kotlin code using springdoc-openapi so I annotated my AnyShape as follows:
#Schema(
oneOf = [AnyShape.Square::class, AnyShape.Circle::class],
discriminatorProperty = "type"
)
sealed class AnyShape {
#Schema(enumAsRef = true)
enum class ShapeType { Square, Circle }
data class Square(val size: Float) : AnyShape() {
val type = ShapeType.Square
}
data class Circle(val radius: Float) : AnyShape() {
val type = ShapeType.Circle
}
}
However the generated yaml spec is not what I'd expect because the main AnyShape is of type object but I'd like to be just the oneOf type (as above)
omponents:
schemas:
AnyShape:
type: object # <<<< THIS
discriminator:
propertyName: type
oneOf:
- $ref: '#/components/schemas/Square'
- $ref: '#/components/schemas/Circle'
Circle:
required:
- radius
- type
type: object
properties:
radius:
type: number
format: float
type:
$ref: '#/components/schemas/ShapeType'
ShapeType:
type: string
enum:
- Square
- Circle
Square:
required:
- size
- type
type: object
properties:
size:
type: number
format: float
type:
$ref: '#/components/schemas/ShapeType'
How could I achieve that the generated yaml is the same as the one I initially wrote manually (1st example)

Related

Parse to different type based on property

I am new to Kotlin, and I'm trying to figure out how parsing unknown JSON works.
I have the following set of classes:
#Serializable
abstract class Node {
val name: String = ""
val type: String = ""
abstract val spec: Any
}
#Serializable
class TestNode : Node() {
override val spec: TestNodeSpec = TestNodeSpec()
}
#Serializable
class TestNodeSpec {
val test: String = "testSpec"
}
I can successfully parse an object directly to a TestNode, but for my purpose I want to be able to read the type to determine the correct class to parse it as. How is this done in Kotlin?

Issues generating an OpenAPI spec using Micronaut-openapi for sealed Kotlin classes

I'm having trouble with sealed classes. I get a specification from Micronaut-openapi, but the code generator I am using (orval) experiences a cyclic reference and fails.
Given this data class:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type",
visible = true
)
#JsonSubTypes(
JsonSubTypes.Type(name = "lounge", value = AnonymousResponse.Lounge::class),
JsonSubTypes.Type(name = "diningRoom", value = AnonymousResponse.DiningRoom::class)
)
sealed class AnonymousResponse {
abstract val id: Int
#JsonTypeName("lounge")
data class Lounge(
override val id: Int,
val hasTv: Boolean,
) : AnonymousResponse()
#JsonTypeName("diningRoom")
data class DiningRoom(
override val id: Int,
val hasTable: Boolean,
) : AnonymousResponse()
}
Micronaut-openapi generates the following components:
components:
schemas:
AnonymousResponse:
type: object
properties:
id:
type: integer
format: int32
discriminator:
propertyName: type
mapping:
lounge: '#/components/schemas/AnonymousResponse.Lounge'
diningRoom: '#/components/schemas/AnonymousResponse.DiningRoom'
oneOf:
- $ref: '#/components/schemas/AnonymousResponse.Lounge'
- $ref: '#/components/schemas/AnonymousResponse.DiningRoom'
AnonymousResponse.DiningRoom:
allOf:
- $ref: '#/components/schemas/AnonymousResponse'
- required:
- hasTable
- id
type: object
properties:
id:
type: integer
format: int32
hasTable:
type: boolean
AnonymousResponse.Lounge:
allOf:
- $ref: '#/components/schemas/AnonymousResponse'
- required:
- hasTv
- id
type: object
properties:
id:
type: integer
format: int32
hasTv:
type: boolean
Which leads to the following error in orval:
src/models/anonymousResponseDiningRoom.ts:10:13 - error TS2456: Type alias 'AnonymousResponseDiningRoom' circularly references itself.
10 export type AnonymousResponseDiningRoom = AnonymousResponse & AnonymousResponseDiningRoomAllOf;
~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/models/anonymousResponse.ts:11:13 - error TS2456: Type alias 'AnonymousResponse' circularly references itself.
11 export type AnonymousResponse = AnonymousResponseLounge | AnonymousResponseDiningRoom | AnonymousResponseOneOf;
~~~~~~~~~~~~~~~~~
src/models/anonymousResponseLounge.ts:10:13 - error TS2456: Type alias 'AnonymousResponseLounge' circularly references itself.
10 export type AnonymousResponseLounge = AnonymousResponse & AnonymousResponseLoungeAllOf;
~~~~~~~~~~~~~~~~~~~~~~~
I am not entirely sure whether it's the specification generator or the code generator doing something wrong, but the "allOf" - AnonymousResponse references looks iffy to me, as (at least from how I read it) it would lead to e.g. Lounge also containing information from DiningRoom?
After trying placing a #Schema annotation on the sealed class, and looking into how to customise the automatic Schema generation, I realised this worked:
// No Schema here
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type",
visible = true
)
#JsonSubTypes(
JsonSubTypes.Type(name = "lounge", value = AnonymousResponse.Lounge::class),
JsonSubTypes.Type(name = "diningRoom", value = AnonymousResponse.DiningRoom::class)
)
sealed class AnonymousResponse {
abstract val id: Int
#Schema // Schema here
data class Lounge(
override val id: Int,
val hasTv: Boolean,
) : AnonymousResponse()
#Schema // Schema here
data class DiningRoom(
override val id: Int,
val hasTable: Boolean,
) : AnonymousResponse()
}
which ultimately resulted in the following orval code:
anonymousResponse.ts
import type { AnonymousResponseLounge } from './anonymousResponseLounge';
import type { AnonymousResponseDiningRoom } from './anonymousResponseDiningRoom';
import type { AnonymousResponseOneOf } from './anonymousResponseOneOf';
export type AnonymousResponse = AnonymousResponseLounge | AnonymousResponseDiningRoom | AnonymousResponseOneOf;
anonymousResponseLounge.ts
export interface AnonymousResponseLounge {
id: number;
hasTv: boolean;
type?: string;
}
anonymousResponseDiningRoom.ts
export interface AnonymousResponseLounge {
id: number;
hasTv: boolean;
type?: string;
}
anonymousResponseOneOf.ts
export type AnonymousResponseOneOf = {
id?: number;
};
And while it doesn't have constant values for the discriminator types, I don't think this is the fault of the spec.

Serializing Non-Data Classes in Kotlin

In a Kotlin library, I have the following class that I want to serialize (preferably with kotlinx):
class A(
id: PropertyValue<String>,
archived: PropertyValue<Boolean>
) {
val id: String by id
val archived: Boolean by archived
constructor(
id: String,
archived: Boolean,
) : this(PropertyValue.Value(id), PropertyValue.Value(archived))
}
However, Json.encodeToString method won't work, because this class does not have any serializer. How can I serialize this class?

Generic Base Respose models in Kotlin

Everyone following is my json response:
{
"requestResponse": {
"status": 1,
"result": true,
"msg": "Success"
},
"userId": 5504
}
And following is my Base Response class:
class BaseResponses<T>{
lateinit var requestResponse: RequestResponse
}
and following are my User data class parameters.
data class User(val userId:Int)
And below as implementation:
#POST(ApiUrls.CREATE_USER)
fun createUser(#Body body: CreateUser): Single<BaseResponses<User>>
my question is that how can I access T type which is User in the Base class would highly appreciate the help.
Thanks
You don't need a genetic type - you need to inherit the properties.
data class BaseResponses { // Remove T, it's doing nothing
lateinit var requestResponse: RequestResponse
}
// Extend base class to inherit common `requestResponse` field
data class User(val userId:Int) : BaseResponses()
// User now will contain requestResponse object
#POST(ApiUrls.CREATE_USER)
fun createUser(#Body body: CreateUser): Single<User>
I might be understanding you wrong, you just want to re-use the RequestResponse class since it is generic and will be common in all your APIs. So just have it as a parameter in User data class.
So it will be like this
data class User(
val requestResponse: RequestResponse,
val userId: Int
)
Now you can simply access it directly from User object. You can even go a step further and assign it default values like this
data class User(
val requestResponse: RequestResponse = RequestResponse(),
val userId: Int = 0
)
data class RequestResponse(
val msg: String = "",
val result: Boolean = false,
val status: Int = 0
)

Define common properties without inheritance

Is there a way of defining common properties without using inheritance in Kotlin?
For example
If I have two classes that both require an "id" property.
class Dog() {
var id: UUID?
}
class Cat() {
var id: UUID?
}
The general JAVA way to solve this is introduce a super class
class Animal() {
var id: UUID?
}
class Dog: Animal()
class Cat: Animal()
But now "Dog" and "Cat" are of type "Animal". What if I introduce a "Chair" class that also requires a unique identifier.
Essentially what I want to the ability to create a set of properties I can include in a number of different classes for programming convenience only. I don't want all the problems associated with inheritance.
You can, of course, use an interface instead of a base class:
interface HasId {
val id: UUID
}
data class Dog(override val id: UUID) : HasId
data class Cat(override val id: UUID) : HasId
However, the above is still using inheritance. If you have more common properties that would be used in multiple classes it may be a sign that they should be grouped together to form a separate value object e.g.
data class Address(val city: String, val street: String, val country: String)
class Person(val name: String, val address: Address)
class School(val name: String, val address: Address, val studentsCount: Int)
And if you want to treat Person and School uniformly with regards to address property you can still use the interface to denote the common attribute:
interface HasAddress {
val address: Address
}
class Person(val name: String,
override val address: Address) : HasAddress
class School(val name: String,
override val address: Address,
val studentsCount: Int) : HasAddress
It might be possible that delegation will suit your needs:
interface WithId {
var id: Int
}
class IdStorage : WithId {
override var id: Int = 0
}
class Dog(withId: WithId) : WithId by withId {
constructor() : this(IdStorage()) {}
}
class Cat(withId: WithId) : WithId by withId {
constructor() : this(IdStorage()) {}
}
This code is rather verbose, but what it allows you to do is:
Avoid using superclass just for the sake of having id property, which allows you to extend other classes if you need
Usage of interface, which guarantees other pieces of code that your class has id
Allows to move implementation of your properties (or functions) to separate class, hence no need for duplicate code in case of complex property/function implementation
Allows implementing multiple properties/functions in a separate class
As was mentioned in the comments:
interface Animal {
var id: UUID?
}
class Dog: Animal
class Cat: Animal