Guide
Upgrading to 0.7
What changed in Zinc 0.7 and its patch releases. Zinc checks validate tags itself, the OpenAPI spec claims only the rules something enforces, 0.7.1 tidies the spec's names, and 0.7.2 and 0.7.3 fix bugs in binding, routing, statuses and middleware.
0.7 makes the spec a contract: every rule it states is a rule a request is checked against. Update the module and run your tests:
go get github.com/0mjs/zinc@v0.7.3go test ./...What might need a change
Section titled “What might need a change”Validate tags are checked without a validator
Section titled “Validate tags are checked without a validator”An app without Config.Validator used to ignore validate tags. Zinc now checks them with its built-in rules, the common go-playground ones, so a request that breaks a rule gets a 422 naming the field:
# {"error":{"status":422,"message":"validation failed","fields":{"email":"is required","password":"must be at least 12 characters"}}}If your structs carry validate tags you didn’t mean to enforce, remove them. A tag with a rule Zinc doesn’t have, such as alphanum or dive, makes its typed handler panic at registration:
panic: zinc: Zinc's built-in validator doesn't enforce these validate rules: main.Code.Value: alphanum; use rules it declares, or a Validator that declares them with RuleSetUse a rule Zinc has, or plug in go-playground/validator for that rule.
A validator lists the rules it enforces
Section titled “A validator lists the rules it enforces”With Config.Validator set, the spec used to claim every rule in your tags. Now it claims only those your validator lists in a RuleSet() []string method, and a typed handler using another rule panics at registration. Without RuleSet your validator still runs, but the spec claims no rules. Add one to your adapter:
func (playground) RuleSet() []string { return []string{"required", "omitempty", "min", "max", "len", "oneof", "email", "uuid", "url", "dive"}}Enum values are checked
Section titled “Enum values are checked”Values listed with an enum tag or an EnumProvider type were documented but not checked. They’re checked now, with any validator: a value outside the list gets a 422. A zero value, a field left out, still passes unless a rule requires it.
The spec is more exact
Section titled “The spec is more exact”- A field whose rules reject a missing value, such as
oneof=cat dogormin=1withoutomitemptyor adefault, is listed as required. Addomitemptyto make it optional. 422is listed only on routes that can send it: input with rules or enums, or an app with its ownValidator.
Generated clients may change with these: fields that become required lose their pointer.
0.7.1: component names start with a capital
Section titled “0.7.1: component names start with a capital”A component used to keep its Go type’s name as written, so an unexported createInput was createInputInput and a clash was flags.createInput. Now:
| Go type | 0.7.0 | 0.7.1 |
|---|---|---|
pet in a response |
pet |
Pet |
createPetInput as a request |
createPetInputInput |
CreatePetInput |
a second createPet, in package admin |
admin.createPet |
AdminCreatePet |
A generated client’s type names may change with them; most generators already capitalized, so many won’t. A name you chose with OpenAPIName is kept as you wrote it, without a repeated suffix.
0.7.1: header names are spelled as you tag them
Section titled “0.7.1: header names are spelled as you tag them”The spec used to show headers in Go’s canonical form, such as Etag for header:"ETag" and X-Csrf-Token for header:"X-CSRF-Token". It now spells them as the tag does. Header names aren’t case-sensitive, so clients keep working; a test that compares the spec’s text may need the new spelling.
0.7.2: binding fixes
Section titled “0.7.2: binding fixes”0.7.2 fixes bugs. Each item below changes what a program does only where the old behavior was wrong.
- A body can’t fill a field tagged only for the path, query, headers or cookies, whatever the key’s spelling or the decoder. An escaped key such as
{"\u0072ole":"admin"}, or a configured decoder that maps its own key names, could set one. - A
defaulttag applies to a field from any source. A body field’s default was never set, and a path field’s was set only sometimes. A body that leaves the key out now gets the default;{"count":0}still sets 0. - A form field comes from the form body only. A URL-encoded request also read the query string, so
?name=xcould fill aform:"name"field. Content-Typematches whatever its case:Application/JSONis JSON, where it was a400.- A struct binds its path and query when the body is
text/plain. All its fields used to be skipped. A non-empty text body, which can’t fill a struct, is a400.
0.7.2: validation checks everything it describes
Section titled “0.7.2: validation checks everything it describes”- Recursive types are checked at every depth: an empty
namein a tree’s grandchild is a422namingchild.child.name, where only the top level was checked. A value that refers back to itself is checked once. - An
enumon a slice checks each element, naming it asroles[0]. It used to check nothing. - An
enumvalue the field can’t hold, such asenum:"lots"on anint, panics at registration for a typed handler and is reported byapp.Validate(). It used to be ignored. - Number limits are exact:
max=9007199254740992on auint64rejects9007199254740993, which it used to let through. A limit the field can’t hold, such asmin=-1on auint, fails at registration. - Rules run in the order you write them, and
omitemptyskips only the rules after it, as go-playground’s validator does:required,omitemptyrejects"". A non-nil pointer counts as set, so a*boolholdingfalsepassesrequired. uuid4requires lower case, anduuid4_rfc4122checks the RFC 4122 variant.
0.7.2: routing
Section titled “0.7.2: routing”- A route for one method never hides another method’s route.
POST /x/123/reachesPOST /x/{id}/besideGET /x/{id}, where it was a405. - A rest-of-path value keeps the path as sent, trailing slash included:
/files/docs/givesdocs/, where it gavedocs. - A route registered with a trailing slash, such as
/users/{id}/, is reached without it unlessStrictRoutingis on. So/users/{id}and/users/{id}/together now panic at startup, naming both. app.URLreturns an error when the URL it would build reaches a different route or different values.FindRoutefinds theGETroute for aHEADrequest, as the app answers it.- A
nilhandler or middleware, or a route with more than 256 parameters, panics at registration, where it used to fail on the first request.
0.7.2: typed handlers send the status the spec documents
Section titled “0.7.2: typed handlers send the status the spec documents”- A
NoContentoutput on a route declaredStatus(200)sends 200, as the spec says, where it sent 204. - A
NoContentoutput keeps a status middleware set withc.Status, where it replaced it with 204. - A
Redirectoutput on a route declared with a status that isn’t a redirect, such asStatus(201), panics at registration, naming the route. It used to send 302 while the spec said 201. - When an output’s body fails to encode, its header fields, such as
Set-CookieorLocation, are no longer sent on the500.
0.7.2: middleware
Section titled “0.7.2: middleware”Skipkeeps each middleware’s own behavior. Wrapping a rewrite inSkipmade everySkip-wrapped middleware count as a rewrite, so putting one on a group panicked. Wrapping CORS made everySkip-wrapped middleware, auth included, run on automaticOPTIONSrequests.- A redirect built from the request path stays on this site. Trailing Slash in redirect mode and redirect rules with a wildcard sent
GET /%2Fevil.example/to//evil.example. Targets now keep the path’s escaping, so a space is sent as%20, and a local target always starts with a single/. A rule whose target names a scheme or host, such ashttps://example.com/*, still redirects there.
0.7.3: middleware and static files
Section titled “0.7.3: middleware and static files”0.7.3 fixes bugs, as 0.7.2 did; each item changes behavior only where the old behavior was wrong.
- Overlapping rules pick one winner. With Rewrite, Redirect or Proxy rules
/*and/api/*, a request for/api/petswent to either target, changing from one request to the next. An exact rule now wins, then the*rule with the longest prefix. A key ending in*is always a prefix rule, never also an exact one. - Proxy regex rules run in a fixed order, the longest pattern first, and
proxy.Newpanics when two rules have the same pattern. - The proxy keeps the request path’s escaping.
%2Freaches the upstream as%2F, where it was decoded to/. An upstream that relied on the decoding sees the encoded path. - Compress finishes the response when a handler panics, so the client gets a complete body rather than a broken gzip stream; recovery then runs as before. A gzipped response’s strong
ETagis sent weak (W/"…"), and a206or any response withContent-Rangeisn’t compressed. c.NegotiateaddsAccepttoVary, so a shared cache doesn’t serve JSON to a client that asked for text.- A static directory redirects to its URL with a trailing slash:
/assets/docsanswers301to/assets/docs/, ashttp.FileServerdoes, so the index’s relative links work. Trailing Slash leaves that slash alone. - The built-in
/openapi.jsonand/docsanswer under a mount at/.app.Static("/", dir)used to hide them. A mount above them gives way forGETandHEAD; a route, or a mount at exactly that path, still takes it. A file namedopenapi.jsonordocsat a static root is no longer served at that path. - Standard middleware on a group or route sees the matched route:
r.PathValueandr.Pattern, such as"GET /items/{id}", ashttp.ServeMuxsets them. Standard handlers getr.Patterntoo. - Trailing Slash can’t go on a group. Like Rewrite and Redirect, it changes the path before routing, so
Group.Use(trailingslash.New())panics at startup; useapp.Use.
What’s new
Section titled “What’s new”- Validation without a dependency:
required,min,max,len,gt,gte,lt,lte,oneof,email,uuid,urland more, through nested structs, slices and maps, with each field named as the client sent it. Config.ValidateResponseschecks typed handlers’ outputs before they’re sent, for development and tests.app.Validate()builds every spec and checks the routes without a request, so a mistake fails a test or deploy.- Spec hooks:
OpenAPIConfig.Extensions,Route.OperationandOpenAPIConfig.Mutateedit the spec as JSON, and the result is checked again. - The request logger passes the request’s context to
slog, so a handler that reads trace IDs from it can add them. - A Production Service: one program with limits, timeouts, request IDs, validation, a protected spec and graceful shutdown.
- 0.7.1:
patterntags: a regular expression a string must match, checked with any validator and listed in the spec. - 0.7.1: Describe what middleware adds:
App.Document,Group.DocumentandRoute.Documentadd a middleware’s credentials, headers and errors to the spec, and the CSRF, Timeout, Limiter and Body Limit middleware each have aDoc(). - 0.7.1:
SchemaNamernames enum types as well as structs; it always could, and the docs now say so. - 0.7.2: The Limiter middleware sends
Retry-Afteron a429: the whole seconds until a request would be allowed, rounded up. - 0.7.2: Security policy: how to report a vulnerability privately.
Not in 0.7
Section titled “Not in 0.7”Zinc doesn’t support polymorphic types, such as a oneOf with a discriminator.
Next steps
Section titled “Next steps”- Upgrading to 0.6: the changes before this release.
- Compatibility: what each kind of release can change.