Migrating to 6.0.0
Everything on this page has shipped, in 6.0.0. It covers two different kinds of break:
- CLI changes. 5.19.0 warned about the one that could be warned
about (
validate --json); the rest are described here. - Library changes, which could not be warned about at all — there is
no mechanism for a Rust compiler warning to say “this will be a
breaking change in a future major version” the way a CLI can print to
stderr. If you depend on
apimock-server,apimock-configorapimock-routingdirectly, rather than only running theapimockbinary, this page is your only notice.
If you are on 5.19.x, the CLI sections are the ones to read first: each names the invocation that changed and what to write instead. If you depend on the crates, read the library sections — those are the breaks a compiler error will surprise you with rather than a message.
Written from 5.19.0 as a preview and revised at the 6.0.0 release, so what you are reading describes the released behaviour rather than an expectation of it.
CLI: apimock validate --json is removed
Shipped in 6.0.0. Covered in depth in the CLI reference and the validate-in-CI guide; summarised here because it’s the one break you can act on immediately.
--json (a bare diagnostics array) was deprecated as of 5.19.0 and is
now removed. --format json, available since 5.19.0, carries the
response shape 6.0.0 keeps — switch to it and verify against a real
binary. Using --json now fails loudly rather than silently changing
what a script parses — exit 2, a message naming --format json as
the replacement:
$ apimock validate --config ./apimock.toml --json
apimock validate: --json was removed in 6.0.0; use --format json instead, which emits the RFC 053 response envelope
Usage: apimock validate --config <apimock.toml> [--strict] [--quiet] [--format text|json]
$ echo $?
2
If --format json was also given alongside --json, the same error
comes back enveloped instead (RFC 053, error.kind: "usage"), rather
than as the plain text above — the caller already asked for
machine-readable output, so the error stays machine-readable too. This
is the one place, across the whole 6.0.0 release, where a removed CLI
flag fails this way (RFC 048 § 7) — the general policy for breaking CLI
invocations at a major version, not specific to this one flag.
match-test’s text output is untouched — 6.0.0 adds --format json
to it rather than reshaping what it prints. Bare apimock keeps
working, and apimock serve is now its explicit spelling — see the
next section.
CLI: an unknown subcommand is now a usage error
Shipped in 6.0.0. A bare word in
the subcommand position that isn’t serve, get, set, match-test
or validate used to silently start a server — apimock banana, a
typo like apimock gte, ran a mock server until killed, with nothing
on stderr to say the word wasn’t recognised. Fixed the same way RFC 059
already fixed an unknown flag: exit 2, stderr names the unknown
subcommand, a near-match suggestion where the edit distance makes one
plausible, no server started:
$ apimock banana
apimock: unknown subcommand 'banana'
$ echo $?
2
$ apimock validat -c apimock.toml
apimock: unknown subcommand 'validat'; did you mean 'validate'?
$ echo $?
2
A flag at the same position (apimock -p 3001, apimock --init) is
unaffected — this only closes the bare-word case a flag typo there was
already caught for. apimock serve is not caught by this fix — see
the next section: it’s a real, intentional subcommand, recognised
before this check ever runs.
CLI: apimock serve is now real
Shipped in 6.0.0. RFC 053
specified apimock serve as the explicit spelling of bare apimock
from the start; it was never built until now.
apimock serve [flags] is identical to bare apimock [flags] in every
respect — same zero-config default, same -c/-p/-d, same --init,
same --help/--version, same failure behaviour for a config that
won’t load. Bare apimock is not deprecated and is not going anywhere;
serve is an addition, never a requirement.
respond.json, and rules written by an earlier apimock set --json
respond now names what kind of body it serves. Alongside file_path
and text there is json, and a rule that uses it is served as
application/json:
[rules.respond]
json = '{"id":1,"name":"ada"}'
A rule declares exactly one of file_path, text and json.
Content-type is derived from that choice, and an explicit
respond.headers.content-type still overrides it — on every one of
them, which was not previously true for .json files (see
Response headers).
text is unchanged and stays text/plain, including when its
content happens to be JSON. That is deliberate: a body that looks like
JSON is not a JSON body.
This matters if you used apimock set --json before 6.0.0. It
wrote respond.text, so those rules serve text/plain; charset=utf-8
— the body is correct, the header is not, and a client calling
.json() under a strict library may reject it. Existing configs are
not rewritten automatically, because silently editing your config on
load is more surprising than the problem it fixes. To fix a rule,
either rename the field:
# before # after
[rules.respond] [rules.respond]
text = '{"id":1}' json = '{"id":1}'
or re-run apimock set --json against it, which now writes json.
Also new: a rule serving a .json file whose contents are not
valid JSON now fails apimock validate and fails to load, instead
of loading and returning 500 on every request. If a config that
worked before now refuses to load, this is the likely reason — the
error names the file and the position. Such a rule could never serve;
apimock now says so at load time rather than per request.
Library: five public structs are now #[non_exhaustive]
Shipped in 6.0.0 (RFC 052) — this
is the one item on this page that has already shipped rather than being
a preview, because main is the 6.0.0 line and the break is real from
this point on for anyone building against it.
TraceConfig, RequestSummary (apimock-server::trace),
ParsedRequest (apimock-routing), LogConfig, and VerboseConfig
(apimock-config) are all pub structs with public fields. Before this
change, constructing one with a struct literal, or exhaustively
destructuring one (let Foo { a, b, c } = value; naming every field),
both compiled from any crate. Now both stop compiling from outside the
type’s defining crate — fields stay publicly readable by name
(value.body_json still works everywhere), only literal construction
and exhaustive destructuring are affected.
What replaces a struct literal, concretely — the two types with a real cross-crate constructor:
ParsedRequest::new(url_path: String, component_parts: hyper::http::request::Parts) -> Selfbuilds one with no body (body_json/body_lenbothNone) — the shape every existing caller outsideapimock-routingactually wanted. Chain.with_body(body_json: Option<Value>, body_len: Option<usize>)to attach one, replacing whatever was there before (it doesn’t merge with a prior call).VerboseConfig::new(header: bool, body: bool) -> Self— aconst fn, so it works in aconstinitializer, which a runtime-only builder would not.LogConfigdidn’t need one: nothing outsideapimock-configever constructed it with a literal — every existing use goes throughDefaultorDeserialize, both untouched by#[non_exhaustive].
TraceConfig and RequestSummary got the attribute but no new
constructor — every construction site for both, checked across the
whole workspace, was already inside apimock-server, the crate that
defines them, so nothing outside that crate was ever affected.
TraceConfig::default() (already existed) remains how to build one from
elsewhere if you need to; a real cross-crate literal site would need
its own constructor the same way ParsedRequest’s did, and none exists
today.
What replaces exhaustive destructuring: match or destructure with
.. to ignore fields you don’t use (let Foo { a, .. } = value;), which
already compiled before this change and keeps compiling after it — the
mechanical fix, if you hit this, is adding ...
Why now, in one release, rather than piecemeal: three RFCs landing
on main this month (040, 050, and the shape of 051’s own configuration
surface) each added fields to one or more of these types, and every one
of those additions was, strictly, a breaking API change that went
unnoticed until asked about directly. RFC 052 takes that break once,
deliberately, instead of repeating it by accident — see RFC 052 itself
for the full reasoning.
Whether the GUI constructs any of these five is still an open question (RFC 052’s Unresolved 1) — the constructors above were built for what this workspace’s own code needs, established from source rather than guessed at. If the GUI turns out to construct one of the three that didn’t get a constructor, that’s an additive addition on top of this shape, not a redesign.
Library: Prefix is now #[non_exhaustive], and respond_dir stopped growing
Shipped in 6.0.0 (RFC 058) — like the five-struct change above, this is live from this point on, not a preview.
The bug. apimock_routing::rule_set::prefix::Prefix::respond_dir_prefix
resolved the directory Respond::file_path is served from, then wrote
that resolved value back into the same field it read the user’s own
respond_dir = "…" from. Since that field is also what got persisted
back to the rule-set TOML, a load-then-save cycle resolved the
already-resolved value again — respond_dir grew by one ./ segment
on every save, without bound ("./." → "././." → "./././." → …).
It shipped in 5.19.0; any tool that loads a workspace and saves it —
apimock set, and the GUI once it lands on this contract — triggered
it. Values already grown by it are semantically unchanged (./././.
and . are the same directory), so nothing using them was ever
actually wrong, just increasingly cluttered on disk.
The fix. respond_dir_prefix now holds only what a person actually
wrote in [prefix] — untouched by loading, and absent entirely
(no [prefix] manufactured) when the file never had one. The resolved
directory the matcher needs lives in a new field,
RuleSet::resolved_respond_dir — read it via RuleSet::dir_prefix(),
unchanged in shape from before this fix, if you were calling that
already.
A file already grown by the bug heals itself, gradually. The next
time a rule set whose respond_dir is purely ./-segments ("./.",
"././.", …) is saved for any other reason, that value collapses to
"." as part of the same write — not a standalone rewrite of files
nobody asked to change. An authored path like respond_dir = "responses"
or "./responses" is never touched by this, only a value that is
provably nothing but the current directory repeated. If you have a rule
set that predates this fix and haven’t triggered a save on it since, its
respond_dir may still read as several ./s stacked up; that’s inert
and can be left alone, cleaned up by hand, or left for the next set/GUI
save to normalise on its own.
Prefix gained #[non_exhaustive] in the same change (it’s pub,
though not re-exported from apimock_routing’s crate root) — construct
one via Deserialize (TOML parsing), the only way anything in this
workspace ever did; a struct literal against Prefix now only compiles
from inside apimock-routing itself.
Prefix::validate also changed signature, in the same fix and for
the same reason: it used to read the resolved directory off self
(pub fn validate(&self, rule_set_idx: usize) -> bool), which only
worked because that was the field this bug overwrote with the resolved
value. Once respond_dir_prefix stopped holding the resolved form,
validate had nowhere left to read it from, so it now takes that
directory as a parameter instead:
pub fn validate(&self, resolved_respond_dir: &str, rule_set_idx: usize) -> bool.
A public-API break for the same reason as the field itself — call it
with rule_set.dir_prefix() for the first argument, the accessor that
already existed for this.
Library: TraceConfig, ParsedRequest, and RequestSummary already had new fields, before #[non_exhaustive]
For the historical record — the reason RFC 052 exists at all. RFC 040
and RFC 050 each added fields to these types before #[non_exhaustive]
existed to absorb that:
TraceConfiggainedheader_redaction,header_denylist,header_allowlist(RFC 040 — request-header redaction for the trace channel).ParsedRequestandRequestSummaryeach gainedbody_len(RFC 050 — a non-JSON request body’s presence and length, never its content).
Both additions predate #[non_exhaustive] landing, so a struct literal
written against an older version of either type would already have
needed updating for this reason alone, independent of the
#[non_exhaustive] change above. This is exactly the class of break
#[non_exhaustive] now exists to prevent recurring.
Library: error variants are boxed, and #[non_exhaustive] now covers the whole public API
Shipped in 6.0.0 (RFC 041) — like the changes above, this is live from this point on, not a preview. It closes the gap this section used to describe as deferred.
The boxing break. ConfigError::ConfigParse.source and
RoutingError::RuleSetParse.source change from toml::de::Error (88
bytes — the sole cause of every clippy::result_large_err suppression
either crate carried) to Box<toml::de::Error> (8 bytes).
Display output and Error::source() are unchanged — #[source]
still reaches through the box — so this is a representation change,
not a behavioural one. If you destructure either variant and bind
source by value expecting toml::de::Error, you now get
Box<toml::de::Error>; dereference it (*source) to get the inner
value back, or call methods through the box as before (Deref
transparently forwards).
The #[non_exhaustive] sweep now covers the whole re-exported public
API, not just the five structs from earlier this page. RFC 052 said
“this is the change that stops the pattern” of a field or variant
addition silently being a breaking change; RFC 041 is that change. The
method: every type named in a pub use at each of the four crates’
lib.rs, minus what RFC 052 and RFC 058 already covered, minus structs
with no public fields (nothing outside the crate could ever construct
those by literal anyway, so the attribute buys nothing) — roughly 43
types. Two consequences, same as the five-struct change above:
- An exhaustive
matchon any of these types, from outside its defining crate, now needs a wildcard arm. - Struct-literal construction (even naming every field) stops
compiling from outside the defining crate. The six error enums are
an exception worth calling out explicitly:
#[non_exhaustive]on anenumrestricts matching, not building its existing variants — a struct-like error variant with every field public stays constructible with ordinaryEnumName::Variant { .. }syntax across the crate boundary. Only the struct types below lose literal construction outright.
What replaces a struct literal, for the payload and CLI-argument types that had real cross-crate construction sites:
HeaderConditionPayload::new(name, op) -> SelfandBodyConditionPayload::new(kind, path, op, value) -> Self— the RFC’s own required cases: both types lackedDefaultbefore this change (avaluefield with no meaningful empty state), so they needed one built from the fields that actually matter;valueonHeaderConditionPayloadstarts unset (None) since it’s only required for some operators, assign it afterwards.RulePayload,RespondPayload,NodeValidationalready derivedDefault; nothing new to build —Default::default()then assign the fields you need, the same pattern asTraceConfigabove.ValidationIssue::new(severity, message),Diagnostic::new(severity, message)(itsnode_id/filestart unset — assign them for a diagnostic scoped to a node or file),DiffItem::new(kind, target, summary), andConditionWithId::new(id, view)— none of these had a meaningful “empty” state (every field matters), so each got a constructor instead ofDefault.ValidationReportdid have a meaningful default — its existingValidationReport::ok()is that state (no diagnostics, valid) — so it also gainedimpl Defaultthat callsok(), letting it join theDefault::default()-then-assign pattern too.EnvArgs::empty() -> Self(every fieldNone) — not callednewordefaultbecause both names were already taken:EnvArgshas a pre-existing, unrelatedpub fn default() -> AppResult<Option<Self>>that parsesenv::args()and is fallible, kept under#[allow(clippy::should_implement_trait)]since renaming it would itself be a breaking change.- Everything else in the sweep —
Config,ListenerConfig,ServiceConfig,NodeId,ReloadHint(both theapimock-configstruct and theapimock-serverenum form),MiddlewareHandler,Server,App, and every fieldless-variant enum (Severity,BodyOp,HeaderOp,UrlPathOp,NodeKind,ConfigFileKind,DiffKind,BodyConditionKind,ServerState) — either already had a working constructor (Default, or anew/compilethat was already the only way anything in this workspace built one) or, for the fieldless enums, needed nothing at all: unit variants are always constructible by name regardless of#[non_exhaustive].AppStategainedAppState::new(config, middlewares, tracer)since it had neither before.
One class of type got the attribute and deliberately no
constructor: library-produced view and result types —
ServerHandle, ApplyResult, SaveResult, ConfigFileView,
ConfigNodeView, RuleSetView, RuleView, HeaderConditionView,
BodyConditionView, UrlPathView, RouteMatchView, MatchedRule,
MatchConsidered, RouteValidationIssue, FileTreeView,
FileNodeView, ScriptRouteView. Nothing in this workspace builds one
of these by hand — every one comes back from a call
(Workspace::apply, Workspace::save, Workspace::snapshot, a route
match, …), never gets constructed to go into one. If a test you own
built one of these with a struct literal to fake a return value, that
stops compiling; there is no Default/new() replacement, by design
— adding constructors nobody calls in production just to satisfy a
test would be the wrong fix. Drive the real call that produces the
value instead (run the Workspace operation, or the route match, and
assert on what it returns), or hold onto a value the library already
handed you rather than reconstructing one.
The six error enums also gain kind(). Since #[non_exhaustive]
forces every downstream match to carry a wildcard arm, a caller with
no other way to branch on failure class would otherwise fall back to
matching on Display text — worse than before. Each of ConfigError,
WorkspaceError, ApplyError, SaveError, RoutingError, and
ServerError gains a .kind() method returning its own
#[non_exhaustive] *Kind enum, one kind per variant:
#![allow(unused)]
fn main() {
match err.kind() {
ConfigErrorKind::Parse => { /* … */ }
ConfigErrorKind::RuleSet => { /* … */ }
_ => { /* … */ }
}
}
This is a separate taxonomy from apimock::cmd::envelope::ErrorKind
(the CLI’s published, schema-versioned contract) — the two are not
fused, and neither delegates to the other. WorkspaceErrorKind in
particular does not delegate to ConfigErrorKind even though
WorkspaceError::Config wraps a ConfigError: match on source() if
you need the inner detail.
What isn’t changing
Worth stating plainly, since a migration page can read as longer than it
is: the exit-code scheme (0/1/2, set in RFC 049) is not
changing — no new code was introduced. Specific invocations did move
within it this cycle: a subcommand flag given no value, previously
0 or 1 depending on the flag, is now always 2 (RFC 064). Stream
discipline (diagnostics to stderr, machine-readable output to stdout) is
not changing. validate’s own diagnostics, severities, and exit codes
are not changing — only --format json’s wrapping shape around them is
new. Nothing about how a mock server matches or responds to requests is
changing.