Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sealed class support #5

Closed
Globegitter opened this issue Dec 3, 2019 · 10 comments
Closed

sealed class support #5

Globegitter opened this issue Dec 3, 2019 · 10 comments

Comments

@Globegitter
Copy link

Globegitter commented Dec 3, 2019

Hey, thanks for this project, so far it looks great but I am running into some limitations with sealed classes. I have a model as follows:

data class Tablet(
    val id: String,
    val something: Something,
    val apps: Map<String, App>
)

where-as app is a sealed class and while jackson 2.10.1 recognises that and converts incoming requests perfectly fine in the generated json schema App is only displayed as an empty object. Are sealed classes meant to be supported? As far as I can tell that would probably map to a oneof, right?

Edit: For reference, here is btw the PR that added auto-discovery of sealed classes: FasterXML/jackson-module-kotlin#240

@Wicpar
Copy link
Collaborator

Wicpar commented Dec 3, 2019

indeed you should have a warning in the log "No public properties found in object $type"
i can try and add sealed class support right now.

@Globegitter
Copy link
Author

Globegitter commented Dec 3, 2019

@Wicpar Ah yeah, that would be great indeed. I gave it a quick try by adding to SimpleSchemaRegistrar.kt:

private fun SchemaRegistrar.makeSealedSchema(type: KType) = type.jvmErasure.sealedSubclasses
        .let { props ->
            props
                .filter { it.visibility == KVisibility.PUBLIC }
                .associate {
                    Pair(it.java.name, get(it.java.toKType()).schema)
                }
                .let { properties ->
                    if (properties.isEmpty()) log.warn("No public properties found in object $type")
                    Schema.SchemaObj<Any>(
                        properties,
                        props.filter { !it.java.toKType().isMarkedNullable }.map { it.java.name })
                }
        }

and it does now show the classes in the swagger UI. But this was a complete blind stab, so not sure if that makes complete sense and also not sure if that is the best way to display it in the UI. So if you could take a look that would be really appreciated.

@Wicpar
Copy link
Collaborator

Wicpar commented Dec 3, 2019

@Globegitter
since sealed classes are a special case for objects, the implementation would actually be replacing:

    private fun SchemaRegistrar.makeObjectSchema(type: KType): Schema<*> {
        val erasure = type.jvmErasure
        if (erasure.isSealed) {
            return Schema.OneSchemaOf(erasure.sealedSubclasses.map { get(it.starProjectedType).schema })
        }
        val props = erasure.declaredMemberProperties.filter { it.visibility == KVisibility.PUBLIC }
        val properties = props.associate {
            Pair(it.name, get(it.returnType).schema)
        }
        if (properties.isEmpty()) log.warn("No public properties found in object $type")
        return Schema.SchemaObj<Any>(
            properties,
            props.filter { !it.returnType.isMarkedNullable }.map { it.name })
    }

it works with the description, but jackson is not able to parse it properly through call.receive

This implementation supports circular definitions in the model.

@Globegitter
Copy link
Author

Ah yeah, that is already looking much better - thanks. I am not sure I fully understand the comment but jackson is not able to parse it properly through call.receive though.

@Wicpar
Copy link
Collaborator

Wicpar commented Dec 3, 2019

Do you simply send object based on a subclass or also want to receive them?
Sending works fine, but receiving throws a jackson exception.

@Globegitter
Copy link
Author

I do want to receive them, i.e. this is the request json for a post endpoint. But I have tried this change and at least in my unit test it is working fine with sending the json as a string and me receiving it as the correct object.

@Wicpar
Copy link
Collaborator

Wicpar commented Dec 3, 2019

In my tests:

...
                route("sealed") {
                    post<Unit, Base, Base>(
                            info("Sealed class Endpoint", "This is a Sealed class Endpoint"),
                            exampleRequest = Base.A("Hi"),
                            exampleResponse = Base.A("Hi")
                    ) { params, base ->
                        respond(base)
                    }
                }
...

    sealed class Base {
        data class A(val str: String) : Base()
        data class B(val i: Int) : Base()
        data class C(val l: Long) : Base()
    }

does not work when receiving a json request body that is A, B, or C.

@Globegitter
Copy link
Author

Globegitter commented Dec 3, 2019

You would have to add e.g.:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
sealed class Base {
        data class A(val str: String) : Base()
        data class B(val i: Int) : Base()
        data class C(val l: Long) : Base()
}

and then make sure the json contains an extra @type field with the name of the class, so e.g. A

And make sure to use jackson 2.10.1

@Wicpar
Copy link
Collaborator

Wicpar commented Dec 3, 2019

i got it to work with:

    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
    @JsonSubTypes(
            JsonSubTypes.Type(Base.A::class, name = "a"),
            JsonSubTypes.Type(Base.B::class, name = "b"),
            JsonSubTypes.Type(Base.C::class, name = "c")
    )
    sealed class Base {
        class A(val str: String) : Base()
        class B(val i: Int) : Base()
        class C(val l: Long) : Base()
    }

@Wicpar
Copy link
Collaborator

Wicpar commented Dec 3, 2019

i have pushed the modifications.
c2b7932

@Wicpar Wicpar closed this as completed Dec 3, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants