huma icon indicating copy to clipboard operation
huma copied to clipboard

#560: Add reusing of primitive-based types in the generated OAS.

Open superstas opened this issue 8 months ago • 4 comments

Hey @danielgtaylor

This PR proposes a solution for the issue #560.

We tested this change internally, and it decreased the size of our generated OAS by ~22%.

Conversely, this change may look backward-incompatible ( inlined types are being replaced by $ref ), so I implemented it as the registry option.

Could you please look and share your thoughts on what could be improved or what I overlooked?

Thank you.

superstas avatar Mar 12 '25 14:03 superstas

Codecov Report

All modified and coverable lines are covered by tests :white_check_mark:

Project coverage is 93.02%. Comparing base (6f2a42b) to head (233a02e).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #758      +/-   ##
==========================================
+ Coverage   93.00%   93.02%   +0.01%     
==========================================
  Files          23       23              
  Lines        5250     5265      +15     
==========================================
+ Hits         4883     4898      +15     
  Misses        315      315              
  Partials       52       52              

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

codecov[bot] avatar Mar 12 '25 14:03 codecov[bot]

@superstas thanks this is a nice approach! I also think using a field tag could work to determine when to use a ref vs. when not to (important for refactoring without breaking clients potentially) instead of having it be all-or-nothing at the registry level.

One scenario that seems problematic though is this:

type MyCustomType int

type ResponseBody struct {
  Field1 MyCustomType `json:"field1" enum="1,2,3"`
  Field2 MyCustomType `json:"field2" minimum:"5" multipleOf="5"`
}

How would this get represented in the JSON schema if you are creating refs?

danielgtaylor avatar Mar 15 '25 18:03 danielgtaylor

@danielgtaylor, an approach with a field tag looks good; I'll try implementing it in a separate commit.

As for your question:

How would this get represented in the JSON schema if you are creating refs?

Well, according to the spec (ref), there can only be the summary and description fields. A situation like this is a conflict that should be shown to the user in some way.

Possible solutions:

  • Use $ref only for the very 1st field and keep the rest untouched
  • Keep all the conflict cases untouched

WDYT?

superstas avatar Mar 17 '25 17:03 superstas

@superstas I'm not sure how to solve this honestly. Maybe if there is additional validation on the struct field via tags then you just cannot use a $ref and must generate it inline. So maybe a $ref is only generated when:

  1. You use a named type
  2. There is no additional validation (doc should be okay, maybe example is okay?)

So if you want to use validation your custom primitive type needs to implement huma.SchemaProvider or huma.SchemaTransformer and your struct fields shouldn't add any validation.

type MyEnum string

var _ huma.SchemaTransformer = (*MyEnum)(nil)

func (e MyEnum) TransformSchema(r huma.Registry, s *huma.Schema) *huma.Schema {
	s.Enum = []any{"one", "two", "three"}
	s.Description = "This is a description of the enum"
	return s
}

// Then later use it:
type MyInput struct {
	Body struct {
		Value MyEnum `json:"value"`
	}
}

Would generate something like:

components:
  schemas:
    MyEnum:
      type: string
      enum: ["one", "two", "three"]
      description: "This is a description of the enum"
    MyInputBody:
      type: object
      properties:
        value:
          $ref: "#/components/schemas/MyEnum

danielgtaylor avatar Mar 18 '25 16:03 danielgtaylor