danielgtaylor/huma

[Bug?]: Inconsistent schemas generation.

Closed this issue · 2 comments

Hey there!

I have faced an interesting issue.

I've got these types:

type Equal[T huma.SchemaProvider] struct {
	Operator string `json:"operator" enum:"EQUAL" doc:"The operator to use for the comparison."`
	Value    T      `json:"value" doc:"The value to compare against."`
}

type In[T huma.SchemaProvider] struct {
	Operator string `json:"operator" enum:"IN" doc:"The operator to use for the comparison."`
	Values   []T    `json:"values" doc:"The values to compare against."`
}

type SpecialString string

func (s SpecialString) Schema(r huma.Registry) *huma.Schema {
	min, max := 1, 255
	return &huma.Schema{
		Type:        "string",
		Description: "A special string type.",
		MaxLength:   &max,
		MinLength:   &min,
	}
}

type Input struct {
	Body struct {
		Name     Equal[SpecialString] `json:"name"`
		Surnames In[SpecialString]    `json:"surnames"`
	}
}

type Output struct {
	Body struct {
		Message string `json:"message"`
	}
}

The OAS I get ( with api.OpenAPI().Yaml() ):

Full spec
components:
  schemas:
    EqualSpecialString:
      additionalProperties: false
      properties:
        operator:
          description: The operator to use for the comparison.
          enum:
            - EQUAL
          type: string
        value:
          description: The value to compare against.
          type: string
      required:
        - operator
        - value
      type: object
    ErrorDetail:
      additionalProperties: false
      properties:
        location:
          description: Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id'
          type: string
        message:
          description: Error message text
          type: string
        value:
          description: The value at the given location
      type: object
    ErrorModel:
      additionalProperties: false
      properties:
        $schema:
          description: A URL to the JSON Schema for this object.
          examples:
            - https://example.com/schemas/ErrorModel.json
          format: uri
          readOnly: true
          type: string
        detail:
          description: A human-readable explanation specific to this occurrence of the problem.
          examples:
            - Property foo is required but is missing.
          type: string
        errors:
          description: Optional list of individual error details
          items:
            $ref: "#/components/schemas/ErrorDetail"
          type: array
        instance:
          description: A URI reference that identifies the specific occurrence of the problem.
          examples:
            - https://example.com/error-log/abc123
          format: uri
          type: string
        status:
          description: HTTP status code
          examples:
            - 400
          format: int64
          type: integer
        title:
          description: A short, human-readable summary of the problem type. This value should not change between occurrences of the error.
          examples:
            - Bad Request
          type: string
        type:
          default: about:blank
          description: A URI reference to human-readable documentation for the error.
          examples:
            - https://example.com/errors/example
          format: uri
          type: string
      type: object
    InSpecialString:
      additionalProperties: false
      properties:
        operator:
          description: The operator to use for the comparison.
          enum:
            - IN
          type: string
        values:
          description: The values to compare against.
          items:
            description: A special string type.
            maxLength: 255
            minLength: 1
            type: string
          type: array
      required:
        - operator
        - values
      type: object
    InputBody:
      additionalProperties: false
      properties:
        $schema:
          description: A URL to the JSON Schema for this object.
          examples:
            - https://example.com/schemas/InputBody.json
          format: uri
          readOnly: true
          type: string
        name:
          $ref: "#/components/schemas/EqualSpecialString"
        surnames:
          $ref: "#/components/schemas/InSpecialString"
      required:
        - name
        - surnames
      type: object
    OutputBody:
      additionalProperties: false
      properties:
        $schema:
          description: A URL to the JSON Schema for this object.
          examples:
            - https://example.com/schemas/OutputBody.json
          format: uri
          readOnly: true
          type: string
        message:
          type: string
      required:
        - message
      type: object
info:
  title: My API
  version: 1.0.0
openapi: 3.1.0
paths:
  /input:
    get:
      operationId: get-input
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InputBody"
        required: true
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OutputBody"
          description: OK
        default:
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorModel"
          description: Error
      summary: Get input

The weird thing is that one of the schemas doesn't have MaxLength and MinLength

    EqualSpecialString:
      additionalProperties: false
      properties:
        operator:
          description: The operator to use for the comparison.
          enum:
            - EQUAL
          type: string
        value:
          description: The value to compare against.
          type: string
      required:
        - operator
        - value
      type: object

whereas another one has it

    InSpecialString:
      additionalProperties: false
      properties:
        operator:
          description: The operator to use for the comparison.
          enum:
            - IN
          type: string
        values:
          description: The values to compare against.
          items:
            description: A special string type.
            maxLength: 255
            minLength: 1
            type: string
          type: array

Given that both schemas are based on the same type with the Schema method, it looks weird.
I'm not sure whether it's a bug or intentional; just want to ask how to make these schemas consistent.

Thank you.

I think it is because T is a slice in your In type. I'm not sure of the correct solution for how to get Huma to dig into your slice, but maybe you need to make a special schema type just for that reason.

But minLength and maxLength are not valid for slices, so that is most likely why it's not coming back.

@krisatverbidio, I'm not sure I got your point.

But minLength and maxLength are not valid for slices
The thing is, In type received minLength and maxLength for each item

type In[T huma.SchemaProvider] struct {
	Operator string `json:"operator" enum:"IN" doc:"The operator to use for the comparison."`
	Values   []T    `json:"values" doc:"The values to compare against."`
}

generates

    InSpecialString:
      additionalProperties: false
      properties:
        operator:
          description: The operator to use for the comparison.
          enum:
            - IN
          type: string
        values:
          description: The values to compare against.
          items:
            description: A special string type.
            maxLength: 255
            minLength: 1
            type: string
          type: array

Which is expected.

But, this one

type Equal[T huma.SchemaProvider] struct {
	Operator string `json:"operator" enum:"EQUAL" doc:"The operator to use for the comparison."`
	Value    T      `json:"value" doc:"The value to compare against."`
}

for some reason doesn't have it.
I would expect to get smth like

    EqualSpecialString:
      additionalProperties: false
      properties:
        operator:
          description: The operator to use for the comparison.
          enum:
            - EQUAL
          type: string
        value:
          description: The value to compare against.
          maxLength: 255 <-- missed
          minLength: 1 <-- missed
          type: string
      required:
        - operator
        - value
      type: object

Does it make sense?