Incorrect `allOf` validation.

Aperta
#787 2 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
35/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Ferma
Stack tecnologico
go
Ambito
api, backend

Direzione di ricerca

Inizia con il riproduttore in main_test.go e il ramo allOf di Validate in validate.go, intorno alle righe 378-382. Esegui go test ./... per confermare la risposta 422, quindi traccia il modo in cui vengono validate il riferimento Base e lo schema dell'indirizzo inline. È completato quando una richiesta contenente name, age e address viene accettata, mentre i vincoli allOf rimangono applicati.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

question

Hi there,

I have the following types:

main.go

type Base struct {
	Name string `json:"name" doc:"The name to compare." minLength:"1" maxLength:"10"`
	Age  int    `json:"age" doc:"The age to compare." minimum:"0" maximum:"100"`
}

type Address string
type Extended struct {
	Base
	Address Address `json:"address" doc:"The address to compare." minLength:"1" maxLength:"100"`
}

func (e Extended) TransformSchema(r huma.Registry, s *huma.Schema) *huma.Schema {
	s.Properties = nil
	s.Type = ""
	s.Description = "Extended Schema."
	s.AllOf = []*huma.Schema{
		r.Schema(reflect.TypeOf(Base{}), true, ""),
		{
			Type: "object",
			Properties: map[string]*huma.Schema{
				"address": r.Schema(reflect.TypeOf(Address("")), true, ""),
			},
		},
	}
	return s
}

type Input struct {
	Body struct {
		Extended
	}
}

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

func addRoutes(api huma.API) {
	huma.Post(api, "/user", func(ctx context.Context, input *Input) (*Output, error) {
		resp := &Output{}
		resp.Body.Message = "It works!"
		return resp, nil
	})
}

Here's the full generated spec.

spec
```yaml
components:
  schemas:
    Base:
      additionalProperties: false
      properties:
        age:
          description: The age to compare.
          format: int64
          maximum: 100
          minimum: 0
          type: integer
        name:
          description: The name to compare.
          maxLength: 10
          minLength: 1
          type: string
      required:
        - name
        - age
      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
            - "null"
        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
    InputBody:
      additionalProperties: false
      allOf:
        - $ref: "#/components/schemas/Base"
        - properties:
            address:
              type: string
          type: object
      description: Extended Schema.
      required:
        - address
        - name
        - age
    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:
  /user:
    post:
      operationId: post-user
      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: Post user



What I'm trying to achieve is a types' structure that generates OAS with allOf that reuses a shared schema.

    InputBody:
      additionalProperties: false
      allOf:
        - $ref: "#/components/schemas/Base"
        - properties:
            address:
              type: string
          type: object
      description: Schema for the DummyFilter.
      required:
        - address
        - name
        - age

    Base:
      additionalProperties: false
      properties:
        age:
          description: The age to compare.
          format: int64
          maximum: 100
          minimum: 0
          type: integer
        name:
          description: The name to compare.
          maxLength: 10
          minLength: 1
          type: string
      required:
        - name
        - age
      type: object

I need such OAS in the following pipeline steps.

The issue

main_test.go

func TestUserCreate(t *testing.T) {
	_, api := humatest.New(t, huma.DefaultConfig("Test API", "1.0.0"))
	addRoutes(api)

	_ = api.Post("/user", map[string]any{
		"name":    "ValidName",
		"age":     1,
		"address": "ValidAddress",
	})
}

When I send a valid request ( with all three fields ) I get a validation error.

$> go test ./...

=== RUN   TestUserCreate
    main_test.go:14: Making request:
        POST /user HTTP/1.1
        Content-Type: application/json
        
        {
          "address": "ValidAddress",
          "age": 1,
          "name": "ValidName"
        }
    main_test.go:14: Got response:
        HTTP/1.1 422 Unprocessable Entity
        Connection: close
        Content-Type: application/problem+json
        Link: </schemas/ErrorModel.json>; rel="describedBy"
        
        {
          "$schema": "https:///schemas/ErrorModel.json",
          "title": "Unprocessable Entity",
          "status": 422,
          "detail": "validation failed",
          "errors": [
            {
              "message": "unexpected property",
              "location": "body.address",
              "value": {
                "address": "ValidAddress",
                "age": 1,
                "name": "ValidName"
              }
            }
          ]
        }

The address field is unknown.

My internal research showed that internally Huma iterates over all allOf schemas by validating input against each of them ( ref ) which doesn't look correctly.

In my opinion, the correct behavior is to merge all allOf schemas into a single one ( at least properties ) and run Validate.

This is how I fixed it internally (a quite naive approach, not well-tested yet )

diff --git a/validate.go b/validate.go
index 336817c..13dbf4f 100644
--- a/validate.go
+++ b/validate.go
@@ -376,9 +376,7 @@ func Validate(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, v any,
 	}
 
 	if s.AllOf != nil {
-		for _, sub := range s.AllOf {
-			Validate(r, sub, path, mode, v, res)
-		}
+		Validate(r, mergeAllOf(s.AllOf, r), path, mode, v, res)
 	}
 
 	if s.Not != nil {
@@ -560,6 +558,30 @@ func Validate(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, v any,
 	}
 }
 
+func mergeAllOf(allOf []*Schema, r Registry) *Schema {
+	if len(allOf) == 0 {
+		return nil
+	}
+
+	var merged *Schema
+	for _, s := range allOf {
+		if s.Ref != "" {
+			s = r.SchemaFromRef(s.Ref)
+		}
+
+		if merged == nil {
+			merged = s
+			continue
+		}
+
+		for k, v := range s.Properties {
+			merged.Properties[k] = v
+		}
+	}
+
+	return merged
+}
+
 func handleArray[T any](r Registry, s *Schema, path *PathBuffer, mode ValidateMode, res *ValidateResult, arr []T) {
 	if s.MinItems != nil {
 		if len(arr) < *s.MinItems {

What do you think? Is there a better approach for handling such cases?

Thank you.

Lingua principale
Go
Stelle
4.5k
Fork
298
Merge medio
5g 10h
PR unite (30g)
21

Preparare l'ambiente

Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di danielgtaylor/huma

Tutte le issue di danielgtaylor/huma

Issue simili

Altre issue su Go

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.