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

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.

Related

How to generate oneOf using springdoc from a Kotlin sealed class

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)

TypeScript: Recovering the "object literal may only specify known properties" check when passing through a generic function

I realize the "object literal may only specify known properties" doesn't work in all cases. In particular, it looks like it doesn't work when I pass the object literal through an identity-like function (playground link)
declare function setUser(arg: {name: string}): void;
declare function identity<T>(v: T): T;
setUser({name: 'a', age: 12}); // Error, good!
setUser(identity({name: 'a', age: 12})); // No error, sad.
const u = {name: 'a', age: 12};
setUser(u); // No error, but I'm used to this case already.
Is there a way to write identity in a way that will get back the error?
In my actual codebase, I'm not using the identity function, but a slight variant (playground link):
declare function setUser(arg: {name: string}): void;
type NonNullable<T> = T extends null ? never : T;
export type NullableToOptional<T> = {
[K in keyof T]: null extends T[K] ? NonNullable<T[K]> | undefined : T[K];
};
export function toOptional<T>(x: T): NullableToOptional<T> {
return x as NullableToOptional<T>;
}
setUser({name: 'a', age: 12}); // Error, good!
setUser(toOptional({name: 'a', age: 12})); // No error, sad.
Here you have a naive implementation. You will find explanation in comments
type Base = { name: string }
// credits goes to https://github.com/microsoft/TypeScript/issues/40248
type IsNever<T> = [T] extends [never] ? true : false;
/**
* Obtain extra keys
*/
type ExtraKeys<T, U> = Exclude<keyof U, keyof T>
type Validator<Obj> = (
Obj extends Base
? (
Base extends Obj
? Obj : (
/**
* If there are no extra keys
*/
IsNever<ExtraKeys<Base, Obj>> extends true
/**
* Return source object
*/
? Obj
/**
* Otherwise return source object where all extra keys are [never]
* It helps TS compiler to highlight only invalid props
*/
: Base & Record<ExtraKeys<Base, Obj>, never>
)
)
: never
)
/**
* Validator - validates the argument
*/
declare function setUser<
Name extends string,
Obj extends { name: Name }
>(arg: Validator<Obj>): void;
type NonNullable<T> = T extends null ? never : T;
export type NullableToOptional<T> = {
[K in keyof T]: null extends T[K] ? NonNullable<T[K]> | undefined : T[K];
};
/**
* We need to infer each key and value pair in order to be alphabetic
* to validate return type
*/
export function toOptional<
Prop extends PropertyKey,
Value extends string | number,
Obj extends Record<Prop, Value>>(x: Obj) {
return x as NullableToOptional<Obj>;
}
setUser({ name: 'a', age: 2 }); // Error
setUser(toOptional({ name: 'a', age: 12 })); // Error
setUser({ name: 'a' }); // Error
setUser(toOptional({ name: 'a' })); // Error
Playground

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
)

Parent with a child in the same type - how to map it?

I don't know how can I correctly convert child of ProductEntity (a type of ProductEntity) to Product.
class ProductEntity(id: EntityID<Int>) : BaseIntEntity(id, Products) {
companion object : BaseIntEntityClass<ProductEntity>(Products)
var name by Products.name
var parentProduct by ProductEntity optionalReferencedOn Products.parentProduct
fun toPojo() = Product(idValue, name, parentProduct?.toPojo())
}
data class Product(
val id: Int,
val name: String,
val parentProduct: Product?
)
At this time I have the error: Type checking has run into a recursive problem.
Can you tell me how to fix it?
I found a solution:
class Product(productEntity: ProductEntity) {
val id: Int = productEntity.idValue
val name: String = productEntity.name
val parentProduct = productEntity.parentProduct.toPojo()
}

Kotlin data class + Gson: optional field

I have the following data class in Kotlin:
import com.google.gson.annotations.SerializedName
data class RouteGroup(
#SerializedName("name") var name: String,
#SerializedName("id") var id: Int
)
Sometimes I need to create an object with both fields, sometimes with only one of them.
How can I do this?
EDIT
This is not the duplicate of this question: Can Kotlin data class have more than one constructor?
That question shows how to set a default value for a field. But in my case, I don't need to serialize the field with the default value. I want a field to be serialized only when I explicitly assign a value to it.
it is easy you have to use the nullable operator
import com.google.gson.annotations.SerializedName
data class RouteGroup #JvmOverloads constructor(
#SerializedName("name") var name: String? = null,
#SerializedName("id") var id: Int? = null
)
You may need something like this:
sealed class RouteGroup
data class RouteGroupWithName(
#SerializedName("name") var name: String
) : RouteGroup()
data class RouteGroupWithId(
#SerializedName("id") var id: Int
) : RouteGroup()
data class RouteGroupWithNameAndId(
#SerializedName("name") var name: String,
#SerializedName("id") var id: Int
) : RouteGroup()
EDIT 1:
Or you can use nullable fields and named parameters like this:
data class RouteGroup(
#SerializedName("name") var name: String? = null,
#SerializedName("id") var id: Int? = null
)
val routeGroupWithName = RouteGroup(name = "example")
val routeGroupWithId = RouteGroup(id = 2)
val routeGroupWithNameAndId = RouteGroup(id = 2, name = "example")