Skip to content

@description behavior is counterintuitive #1203

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

Closed
danielleontiev opened this issue Apr 27, 2021 · 10 comments · Fixed by #3228
Closed

@description behavior is counterintuitive #1203

danielleontiev opened this issue Apr 27, 2021 · 10 comments · Fixed by #3228
Milestone

Comments

@danielleontiev
Copy link
Contributor

Tapir version: 0.18.0-M7

Given the example:

case class Request(
      @description("time from") from: TimeRange,
      @description("time to") to: TimeRange,
      @description("simple field description") simpleField: String
)

Transforming to OpanAPI outputs this:

components:
  schemas:
    Request:
      required:
      - from
      - to
      - simpleField
      type: object
      properties:
        from:
          $ref: '#/components/schemas/TimeRange'
        to:
          $ref: '#/components/schemas/TimeRange'
        simpleField:
          type: string
          description: simple field description
    TimeRange:
      required:
      - timestamp
      - misc
      type: object
      properties:
        timestamp:
          type: integer
        misc:
          type: string
      description: time from
  • simpleField contains the description because it's primitive data type and it's schema is "inlined" into properties
  • from and to are fields with common type of TimeRange and both represented as a $ref to one schema

The issue here that one of descriptions is missing.

The full example could be found here: https://github.com/danielleontiev/tapir-regression

So, I see a couple of ways of solving it

  1. The simplest
    Fail compilation when description is used on the field of non-primitive data type to enforce user to use the @description annotation on the object itself. The fact that annotating field of case class results in appearance of description in the field's case classe's schema is the most counterintuitive in this issue imo. Another option is not to fail every time but only if couple of fields with the same type have @descriptoin (In this case using @description on non-primitive fields will still place description in the object's schema in non-conflicting cases which is bad imo)

  2. Introduce 2 annotations instead of 1 - @fieldDescription and @objectDescription (or smth like this)
    And allow the usage of the first only on fields and the second - only on case classes. When @fieldDescription will be used on field that will be transformed into $ref - fail compilation and suggest to use @objectDescription on the case class

  3. inline schema
    If @description is used on non-primitive field it may be possible to inline the object description. The example above then will look like this:

components:
  schemas:
    Request:
      required:
        - from
        - to
        - simpleField
      type: object
      properties:
        from:
          required:
            - timestamp
            - misc
          type: object
          properties:
            timestamp:
              type: integer
            misc:
              type: string
          description: time from
        to:
          required:
            - timestamp
            - misc
          type: object
          properties:
            timestamp:
              type: integer
            misc:
              type: string
          description: time to
        simpleField:
          type: string
          description: simple field description

Such transformation may be counterintuitive as well and it may be better to somehow control such behavior with another annotation like @inlineSchema or smth like this

@danielleontiev
Copy link
Contributor Author

I think I am able to dig into it and implement but it would be great to discuss which way is the best

@adamw
Copy link
Member

adamw commented Apr 29, 2021

Hm that's a good question - and doesn't really impact annotations only. Just as well you can have two schemas with different descriptions created by hand / customised using .modify. So the question is - should such schemas - for the same datatype, but with different descriptions, be considered the same or distinct?

The best solution here would be to include the description alongside the $ref - refining the schema, and omitting the description in the reference at all. But as far as I understand, this is not yet implemented in the Swagger Editor, but possible with OpenAPI 3.1 (not with OpenAPI 3.0): OAI/OpenAPI-Specification#1514.

What would you expect, as a user? Two components with different names? Inlined schemas?

@danielleontiev
Copy link
Contributor Author

danielleontiev commented Apr 29, 2021

Now I overcome the issue using two schemas with different names - it's OK for me but the main issue that I've found the problem accidentally - I think from the user perspective warning or compilation error saying that I will miss description would be totally ok

Neither automatic generation two schemas with different names or automatic inlining looks obvious.

But inlinig itself looks interesting and could be useful it particular case and in general (f.e. if you use some post-processing for schemas and merge it with schemas created by other people then you are interested in avoiding names conflicts - inlining could solve the problem)

@adamw
Copy link
Member

adamw commented May 7, 2021

Maybe the best solution is to wait until 3.1 support in swagger and use the $ref with the description field feature?

I don't think a compilation error is possible (in general, as schemas can be defined in many ways), so only a runtime warning would be an option. But then we enter the realm of how to issue warnings in the first place - logger dependencies etc.

@adamw
Copy link
Member

adamw commented May 7, 2021

In other words, I don't have a good idea on how to solve this problem right now :)

@xeppaka
Copy link

xeppaka commented Oct 11, 2021

Maybe the best solution is to wait until 3.1 support in swagger and use the $ref with the description field feature?

I don't think a compilation error is possible (in general, as schemas can be defined in many ways), so only a runtime warning would be an option. But then we enter the realm of how to issue warnings in the first place - logger dependencies etc.

Hmm, I think you can use allOf for that case:

components:
  schemas:
    Request:
      required:
      - from
      - to
      - simpleField
      type: object
      properties:
        from:
          allOf:
            - description: time from
            - $ref: '#/components/schemas/TimeRange'
        to:
          allOf:
            - description: time to
            - $ref: '#/components/schemas/TimeRange'
        simpleField:
          type: string
          description: simple field description
    TimeRange:
      required:
      - timestamp
      - misc
      type: object
      properties:
        timestamp:
          type: integer
        misc:
          type: string

@adamw
Copy link
Member

adamw commented Oct 11, 2021

@xeppaka Interesting workaround - thanks :) Seems it works

@xeppaka
Copy link

xeppaka commented Oct 15, 2021

@xeppaka Interesting workaround - thanks :) Seems it works

@adamw Do you plan to implement it? It will be really helpful for me as well :)

@adamw
Copy link
Member

adamw commented Oct 15, 2021

@xeppaka as time permits, yes, but no promises on any timelines ;)

@slavaschmidt
Copy link
Contributor

The same, logically, happens with implicit declarations:

final case class Nested(name: String)
final case class Top(left: Nested, right: Nested)

implicit private def schemaNested: Schema[Nested] = implicitly[Derived[Schema[Nested]]].value
 .modify(_.name)(_.description("The name of the thing"))

implicit private def schemaTop: Schema[Top] = implicitly[Derived[Schema[Top]]].value
 .modify(_.left)(_.description("The left thing"))
 .modify(_.right)(_.description("The right thing"))

implicit val jsonB = jsonFormat1(Nested.apply)
implicit val jsonT = jsonFormat2(Top.apply)

private val jsonBodyLookupRequest = jsonBody[Top].description("The whole structure")

produces

Nested:
  required:
   - name
  type: object
  properties:
     name:
       type: string
       description: The name of the thing
  description: The left thing
Top:
  required:
   - left
   - right
  type: object
  properties:
     left:
       $ref: '#/components/schemas/Nested'
     right:
       $ref: '#/components/schemas/Nested'

Wrapping in allOf is the recommended workaround for OpenApi 3.0.
The OpenApi 3.1 supports $ref siblings directly so this indeed might be an easy way to fix that. It looks like the swagger support is slowly arriving

Thank you very much!

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

Successfully merging a pull request may close this issue.

4 participants