httperr

Validáció

Struct-tag-alapú kérés-validáció go-playground/validatorral, strict middleware-ként futtatva, 422 problem-válaszokkal.

A kérés-validáció a gpsystemben deklaratív: szabályokat írsz a struct mezőire tagekben, a futtatást és a hibarenderelést pedig a httperr/validate végzi. A motor a go-playground/validator v10 (a Go de facto standard validátora); a validate csomag pedig azt köti be úgy, hogy minden bukás RFC 9457 problem-válasz legyen, mezőnkénti hibalistával, handler-kód nélkül.

import "github.com/gp-system/httperr/validate"

A Validator típus

func New(opts ...Option) *Validator

func (v *Validator) Validate(out any) error // validál, bukást *httperr.Problem-mé (422) csomagol
func (v *Validator) Struct(s any) error     // ugyanaz, a binding pipeline-on kívüli használatra nevezve

func Translate(verrs validator.ValidationErrors) []httperr.FieldError

A New a validátort két rögzített defaulttal építi:

  • WithRequiredStructEnabled(): a beágyazott structok required szemantikája a validator v11 default szerint működik;
  • json-tag mezőnevek: a hibákban a mező neve a json tagből jön (customerEmail, nem CustomerEmail), beágyazott structoknál pontozott útvonallal (address.city).

Bukáskor a Validate/Struct nem nyers validator-hibát ad vissza, hanem kész *httperr.Problem-et (422), a Translate-tel httperr.FieldError-okká fordított mezőhibákkal: a hibarenderelőnek így nincs több dolga vele. A Translate a gyakori szabályokhoz (required, email, min, max, oneof, gt, ...) ember-olvasható üzenetet ír; ismeretlen szabálynál a failed rule <tag>=<param> formára esik vissza.

Bekötés: strict middleware a handler előtt

A net/http standard strict szervere (az oapi-codegen generálja az OpenAPI specből, lásd Codegen pipeline) json.Decoder-rel dekódol, validáció nélkül: a validate ezt a rést strict middleware-ként zárja be, a teljes bekötött request objektumot (paraméterek + body) validálja a handler előtt. Egy generált gpsystem projektben a kit server.StrictValidator-a az a vékony adapter, ami egy *validate.Validator-t bedrótoz ebbe a middleware-résbe; a generált register.go automatikusan bedrótozza:

shopapigen.NewStrictHandlerWithOptions(apiHandler,
    []shopapigen.StrictMiddlewareFunc{
        server.StrictValidator[shopapigen.StrictHandlerFunc](nil),
        // ...
    },
    shopapigen.StrictHTTPServerOptions{ /* httperr hookok */ })

A nil argumentum szándékos: a server.StrictValidator(nil) request-időben oldja fel a validátort, előbb a server.WithValidator-ral beállítottat keresve a contextben, és csak utána esve vissza a validate.New()-ra. Így egyetlen opció a main.go-ban az egész alkalmazásra érvényes, a generált kódhoz nem kell nyúlni. Részletek, a policy-enforcementhez viszonyított pontos middleware-sorrenddel együtt: a szerver mag.

Generált projekten kívül a validate.New() bekötése semmi kit-specifikusat nem igényel: hívd a v.Struct(req)-et bárhol a saját net/http handleredben vagy middleware-láncodban, ahol egyébként egy dekódolt kérést validálnál, és add vissza a kapott hibát változatlanul; az már egy *httperr.Problem.

A shop: a PlaceOrder kérése

A rendelésfeladás body-ja három szabályt hordoz:

type placeOrderBody struct {
    ProductID     string `json:"productId" validate:"required,uuid4"`
    Quantity      int    `json:"quantity" validate:"required,gt=0"`
    CustomerEmail string `json:"customerEmail" validate:"required,email"`
}

Egy kérés, ami mindhármat megsérti (quantity: 0, hibás e-mail, hiányzó productId), ezt a választ kapja:

{
  "type": "about:blank",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "One or more fields failed validation.",
  "instance": "/api/v1/shop/orders",
  "errors": [
    { "field": "productId", "rule": "required", "message": "is required" },
    { "field": "quantity", "rule": "gt", "message": "must be greater than 0" },
    { "field": "customerEmail", "rule": "email", "message": "must be a valid email address" }
  ]
}

A rule gépi olvasású (a frontend mezőnként ágazhat el rajta), a message pedig ember-olvasható default: a végleges, lokalizált szöveget jellemzően a kliens rendeli a field + rule párhoz.

A kézzel írt structjaidon (service-input, belső DTO-k) a tageket te írod. A spec-first generált request-modelleknél a tag forrása az OpenAPI x-oapi-codegen-extra-tags extensionje, amit TypeSpecből az @extension decoratorral adsz a mezőre:
model PlaceOrderBody {
  @extension("x-oapi-codegen-extra-tags", #{ validate: "required,gt=0" })
  quantity: int32;
}
Az oapi-codegen ebből validate:"required,gt=0" taget emittál a generált mezőre; a fenti pipeline onnantól ugyanúgy fut rajta.

Saját tagek

A beépített szabálykészleten túl sajátot a WithRegister-rel regisztrálsz, és a server.WithValidator-ral teszed alkalmazás-szintűvé:

v := validate.New(validate.WithRegister(func(v *validator.Validate) {
    _ = v.RegisterValidation("slug", func(fl validator.FieldLevel) bool {
        return slugRe.MatchString(fl.Field().String())
    })
}))

err := server.Run(ctx, cfg.Server, register, server.WithValidator(v))

Ezután a validate:"slug" tag mindenhol működik: a StrictValidator middleware-ben és a kézi Struct hívásokban is. Saját tag üzenete a Translate fallback-formáját kapja (failed rule slug); ha szebb kell, a kliens oldalon rendeld hozzá.

Mezőnevek testreszabása

A default json-tag-alapú nevezés a WithTagNameFunc-kal cserélhető, például ha a hibákban a form taget akarod látni:

v := validate.New(validate.WithTagNameFunc(func(field reflect.StructField) string {
    name, _, _ := strings.Cut(field.Tag.Get("form"), ",")
    return name
}))

A binding pipeline-on kívül

A Struct ugyanazt a hibaformát adja bárhol, service-input, konfiguráció vagy worker-payload ellenőrzésére:

if err := v.Struct(input); err != nil {
    return err // már *httperr.Problem (422), a handler-lánc kész renderelni
}

Nyers validator.ValidationErrors-t (pl. egy közvetlenül használt validator-instance-ból) a Translate-tel fordítasz ugyanerre a mezőhiba-formára.

Amit a struct-tagek nem fednek le

A validate struct-tagek (validate:"required,gt=0") a szintaktikai/formai szabályokat fedik le. Az adatbázist érintő ellenőrzések (például egyediség) nem tagként élnek: azok a service rétegbe mennek, errs hibaként jelentkeznek. Az authorizáció külön kérdés, azt a policy réteg kezeli, nem egy validációs tag.

A kittel

A server.StrictValidator (lásd a szerver mag) pontosan a fent leírt *validate.Validator-t csomagolja: magán a validátoron semmi nem változik attól, hogy egy generált projektben fut-e vagy egy kézzel írt net/http handlerben, csak a hívás helye.

Használt patternek

  • Functional options közvetlenül a becsomagolt típuson (Option func(*validator.Validate)): Design patternek.
  • Adapter + fordítási réteg (Translate: validator.ValidationErrorshttperr.FieldError) a validációs library és a drót-formátum közt.

Kapcsolódó oldalak

  • httperr: Áttekintés: a Problem/FieldError forma, amit ez az oldal termel.
  • Hibaválaszok: a mapping-sor, ami egy validációs Problem-et olvas.
  • Szerver: StrictValidator, WithValidator, és hogy a validáció hol ül a middleware-láncban.
  • Policy-k: az authorizáció, a réteg, amit a validáció szándékosan nem fed le.
Copyright © 2026