What changed
Grouped by where it lands, from the changelog.
Upgrading to 1.10
1.10 removes no exported API. A regenerated project picks up everything below. The files craftgo writes once - main.go, config/, svccontext.go, the logic stubs and the middleware scaffolds - keep what they were written with, so the edits they need are listed here.
Designs craftgo gen refuses
Each of these generated a project that built and ran under 1.9. Fix the design and run craftgo gen again.
- A file that imports or declares anything without a
packageclause:package/missing. Addpackage <name>at its top; a comment-only file needs none. - A lower-case
type,enum,scalar,error,middleware,eventor method name, or type parameter:decl/name-case. Capitalise it; a lower-caseservicename only warns. - A package named
init, or like a predeclared Go name such asint,stringorlen:package/name. Rename the package. scalar When datetime:scalar/bad-primitive. Usedatetimedirectly.- A decorator after a declaration on its line, as in
middleware M @doc("m"): a parse error. Put it on the line above what it decorates. type Twith no body: a parse error. Writetype T {}.- An enum value outside the int64 range, or a
\u{…}escape that names no character: a parse error. - A
@defaultthat breaks a validator of its field or scalar:decorator/conflict. Fix the default or the validator. - Bounds no value meets -
@positive @negative,@gt(5) @lt(5), a field's bound against its scalar's:decorator/empty-rangeordecorator/range. @uniqueItemsover elements with an optional or@nullablemember, or holding adatetime:decorator/typemismatch.- A mixin field and a field of the type sharing a JSON key:
field/name-collision. Rename one, or set its@json. - An error mixin field named
errCode,error,httpStatusorwriteResponseHeaders, or an error body field namedmarshalJSON:field/invalid-go-name. Rename it. - A
fileanywhere but a request's top level - a response, an error body, an event payload, amap<string, file>, another package's type, a generic argument such asBox<file>:binding/file-position. Send the content asbytes, or move thefileto the request. - A
@sensitivefield that a path variable names:path/param-missing. Drop@sensitiveor the variable. - An unknown decorator on an
extend serviceblock:decorator/unknown.@operationIdthere:service/extend-decorator-not-method; put it on the method. - A generic argument on a built-in, as in
s string<int>:generic/non-generic. Drop it. payload Page<string?>:generic/optional-arg, as on a field. Put the?on a field inside the generic.- A bound past the field type's range, as
@multipleOf(18446744073709551616.0)on auint64, or a@defaultpastfloat32on afloat32field:decorator/bound-overflow. - A security scheme an operation names that lacks a field its type needs - an
oauth2flow without the URL its grant needs,httpwithoutscheme,apiKeywithoutinorname,openIdConnectwithout its URL: a run that writes the OpenAPI document stops, naming the scheme and the field. Add it. - A constraint decorator on a struct, generic-instance or
anyfield, such asa Addr @gt(3)orp Page<Item> @maxItems(3), or one an enum's backing type does not take, such as@multipleOfon a string enum, which 1.9 ignored:decorator/typemismatch. Drop it, or constrain the fields inside. @json("-"):decorator/argvalue. Use@sensitiveto keep a field off the wire.- An optional array or map as a map value, as in
map<string, int[]?>:type/map-value. Drop the?; an absent entry reads as empty. @path("rest...")for a{rest...}variable: an error. The variable isrest.@formon a field of a request with nofile:binding/form-without-file. Drop@form; the field rides the JSON body, as it did.- A field named
fillEmpty, which the generatedFillEmptymethod takes:field/invalid-go-name. Rename it. - Two middlewares whose scaffolds write one file, as
APIKeyandApiKey:middleware/collision. Rename one. - A middleware named
ConfigorMiddlewares, whose routes did not compile:decl/go-name-collision. Rename it. - A fractional bound on a field of an int enum, as
@gt(1.5)or@multipleOf(2.5), which 1.9 documented and never checked:decorator/typemismatch. Use a whole number. @minItems,@maxItems,@uniqueItems,@maxSizeor@mimeTypeson a field typed by a type parameter, as intype Box<T> { v T @maxSize(10) }, which 1.9 documented and never checked:decorator/typemismatch. Constrain a concrete field, or the collectionT[].- A method whose file the go command builds only for tests or one system under the file case, as
RunTest(run_test.go) orListWindows(list_windows.go):service/method-file-name. Rename it, or setoutput.fileCasetokebaborcamel. - Methods of one service directory writing one file, as
GetURLbesideGetUrl, or one Go name, asOrderbesideNewOrder, or a method namedLogger, which generated code that did not compile:service/method-name-clash. Rename the method, and move its logic stub to the new file name. - An event payload reaching a field bound to
@path,@query,@header,@cookieor@form, which never reached a consumer:event/payload-binding. Drop the binding, or give the event a type without it.
A decorator as another decorator's argument, @a(@b), is out of the grammar: one parse error at the inner @.
What the server answers
- Every error the framework writes is JSON
{"message": "..."}withContent-Type: application/json; charset=utf-8andX-Content-Type-Options: nosniff: the 404, the 405 (withAllow), a 413, a panic's 500 and the default validation 400 weretext/plain. None of them is in the OpenAPI document. - A request missing a required
fileoranythat a type parameter types, as in aFilePart<file>mixin, answers 400<field>: required; 1.9 passed the service nil. - A required list, map or
bytesthe logic leaves nil goes out as[],{}or"", wherever it sits in a response or an error body, where 1.9 wrotenull, which the document does not allow. An optional one is still left out and a@nullableone is stillnull. The value the logic returns is filled in place the first time: a response object, or an error's lists and values below its body's top level, shared between requests is written to once, a map never, a copy taking its place, and an already filled value is only read. - A body read past its cap answers 413
{"message":"request entity too large"}, where 1.9 answered 400. - A multipart body the parser refuses answers 400 through
SetDefaultValidationFailed, where 1.9 answered 413 (a regenerated handler). - A handler that returns a deadline's error answers 504
{"message":"gateway timeout"}, and a context error after the client has gone writes nothing, the access log recording 499; 1.9 answered 500 and loggedunhandled service error.SetHandleUnknownErrorno longer receives these errors. - A float query, header, cookie or form value of
NaNor±Infanswers 400. - An array
@headerbinds a comma-separated list (X-Ids: 1,2), as the OpenAPI document describes it; a multipart text part no longer takes a query parameter of the same name (regenerated handlers). SetCORSruns ahead of thesrv.Usemiddlewares: a preflight is answered before an auth middleware can refuse it, and the responses those middlewares write carry the CORS headers.- A success response that cannot be encoded, such as one holding a
NaNfloat, answers 500{"message":"internal server error"}and logsunhandled service error, where 1.9 sent the success status with an empty body (a regenerated handler). - Validation texts: a cross-field group lists its members by wire name with no type prefix (
requiresOneOf [primary_email backup_email] - at least one must be set), an enum value outside its set readsstatus: must be one of [open in_progress done](wasstatus: invalid TodoStatus value), a nested failure carries its path (home: rooms: furniture: name: length less than 1), and a body value of the wrong JSON type names its JSON path (c: expected string, got number). A client that matches the old text needs the new. - An error whose body fields are all optional and unset is written as
{}, the body its OpenAPI response declares, where 1.9 wrote the{"code","message"}envelope.
main.go, config.go and the stubs
- A panic line carries the request's trace ids when the server is built with
server.New(svc, server.WithTelemetry(tel.HTTPMiddleware())), as a newmain.godoes. In an existingmain.go, replacesrv.Use(tel.HTTPMiddleware())with that option: keeping both records every span and metric twice. - A new project's
config.gofills onlyserver.addr,grpc.addrandserviceName; the runtime defaults the rest, and an emptymetrics.adminAddrstarts no scrape listener (the generatedconfig.yamlsets":9090"). An existingconfig.gokeeps its own defaults. config.Path()names theconfig.yamlunderoutput.config. Aconfig.gowritten before, withoutput.configmoved, needs itsPath()edited by hand.- A logic stub for a method returning
Page<map<string, Item>[]>keeps the signature it was written with; edit it to*types.Page[[]map[string]types.Item].
Generated code
- Regenerated files change shape, not behaviour: imports in three groups, one-line generated comments, the design's
@docas the Go doc, sizes as shifts (12 << 20), a bare-integer@timeoutas a duration literal, and no bound check the Go type already enforces. - A generated error holds no
codeormessagefield:Error()andErrCode()return constants, and a generatedMarshalJSONwrites its JSON. - An optional map value whose type holds nil, as in
map<string, Blob?>overscalar Blob bytes, is aBlob, not a*Blob.
OpenAPI document
- Components renamed:
[]or?on a map or a generic instance, or on an array of one, leads the name -Page<map<string, Item>[]>isPageOfArrayOfMapOfStringAndItem(wasPageOfMapOfStringAndItem),Page<Box<Item>[]>isPageOfArrayOfBoxOfItem(wasPageOfBoxOfItemArray). A client generated from the document gets the new type names. - A generic instance named only by a
@sensitivefield or a header-inlined response, and the<Method>ReqBodyof a request with nothing on its body, get no component. - A basePath variable is a server variable, where 1.9 listed it as a query parameter or a body property.
- Bodies use the
@jsonkey, every@errorsresponse is kept, and services of one name from every package are listed.
Manifest, CLI and editor
- An unknown manifest key is a warning on stderr and in the editor; a removed key still stops the run.
craftgo gensweeps less. Underoutput.pbit takes only the pb code of the design's own protos, from the directories they write into, and nothing when the design has no proto; next to the OpenAPI document it takes only the document. The pb code of a proto moved to another directory or of a project's last proto, and the old document of a renamedoutput.openapi, stay until you delete them.craftgo fmtsets trailing comments off by one space and keeps every literal as written: run it once to settle the design files (craftgo fmt -llists them).
Runtime API
- New:
server.WithTelemetry,log.Follow,kafka.ErrClosed. SetLoggerinstallslog.DefaultandLogger()returns it, on bothserver.Serverandrpc.Server. TheAccessLog(srv.Logger())of a 1.9main.gowrites to the logger of startup, as it did;AccessLog(log.Follow()), which a newmain.goinstalls, follows a laterSetLogger.- Deprecated, still working:
Server.RegisterMiddleware,Server.With,server.Timeout, theserver.Loggeralias,server.DocsUIand its constants. pkg/events/natsneeds Go 1.25, where it needed 1.26.
From 1.7.1 to 1.9
DSL
event Name { payload T }declares a contract at file level;@contract("subject")sets its wire identity, which defaults to<package>.<Event>.- A payload may be an array of a declared type,
payload T[]: the descriptor is typed on the slice and every element is validated in turn. - A
serviceholds HTTP methods only, and the design names no listener - which events a deployable listens to, on which group, behind which middleware, is Go code in that deployable. - Two reserved words,
eventandpayload, are still usable as identifiers where unambiguous, as is every other reserved word. datetimeis atime.Timein Go and an RFC 3339 string in JSON (format: date-time); body fields only, no validators, no@default.@json("key")on a field sets the JSON key when it is not the field name; the Go tag, the OpenAPI document and validation messages follow it.
Generated output
- One file per DSL package that declares an event,
events.gounderevents.targets[].out: a<Name>Contractconstant and anevents.Event[T]descriptor per event, its@docas the Go comment. output.kind: contractsgenerates only payload types and this library, for a design several deployables import.main.goattaches the design through one generated call,wiring.Register, in awiringpackage (output.wiring) whose surface does not change with the design; the scaffold no longer lists routes.- A design package that declares no type gets no types package, and
validate.gois written only where a type has something to validate. - Stale output is pruned: inside the output directories the manifest names, every file carrying a generated header that the run did not write is deleted, and emptied directories with it.
Runtime
pkg/eventsis a new module:Bus(Use,Register,Start,Plan), typedGroup,Event[T]descriptors that decode and validate before a handler runs, publish options, batch publishing, dispositions, a JSON codec, an in-process transport and access-log middleware.pkg/events/natsadds core NATS and JetStream, with one durable per group carrying the group's filter subjects, per-group settings, redelivery backoff, a delivery cap and NAK hand-back during a rolling deploy.pkg/events/kafka(franz-go, Go 1.25) adds classic and share groups, TLS and SASL options,WithClientOptionsandRecordFrom.log.Slog()returns a*slog.Loggerwriting through craftgo's own logger.
Manifest
- Output keys are checked as a set:
output.transportmay be named anything, two keys may not name one directory, and-is rejected on a key that has no disabled mode. - The routes umbrella is removed with the last route.
events.targets[]names the languages event artefacts are generated for; omit the block and a design that declares events gets one Go target at./internal/events, or./gen/eventsunderoutput.kind: contracts.
Removed, or never shipped
The strip under the diagram lists what the design no longer carries; none of it ever reached a release.
pkg/otelandpkg/metricsare gone - alias-only shims overpkg/telemetry; importpkg/telemetrydirectly.- The listener declaration, the decorator that named its group, the decorator that named its ordering key and the one that named its middleware chain never shipped: the group is an argument to
Subscribe, the key an argument toPublish, the chainbus.Use(...)where the bus is built. - Projections, the AsyncAPI document and the
.craftgo-gen/bookkeeping folder never shipped either.
Fixed
craftgo-lspexits onexit, with status 1 when noshutdownpreceded it, instead of waiting for the client to close its stdin.- The editor reports what
craftgo genreports. - A type whose fields all delegate to another package compiles.
- A
@groupwhose name ends intime, a service namedCraft, a package whose name ends intypes: none breaks its generated files. @securityscheme names are listed in a stable order.
Upgrading
Drop the manifest keys that are gone. design, output.services, output.consumeMiddleware and events.asyncapi are rejected while the manifest is read, each naming what replaced it (<key> is no longer a manifest key - <note>; drop it).
Delete every .craftgo-gen/ folder. Nothing reads them. The // Code generated by craftgo. DO NOT EDIT. header is the whole record now: at the end of a run craftgo deletes every headed file in its output directories that the run did not write.
wiring.Register is optional for an existing main.go. It is generated for every project, but main.go is gen-once and is never rewritten, so a project that already registers its routes keeps working unchanged. Switch the registration line to wiring.Register(ctx, srv, svcCtx) to pick up the startup check that fails for every middleware the design applies and svcCtx leaves nil.
datetime and @json("key") are available. Both are body-field features; see Types and Scalars.
Nothing else needs doing for events. A design that declares no event generates nothing new. When you add one, read Events - the contract goes in the design, and the group, the middleware and the transport go in the deployable.