Summary
devview-networkmock-core currently configures mocks with a bespoke mocks.json format built around an API groups × environments model. This epic tracks replacing that with OpenAPI 3.x as the configuration format, and removing the environment axis in favor of a simpler mocked/live split plus API version awareness.
This issue is a tracking issue only — no code changes here. Each linked issue is independently implementable and reviewable.
Why
-
The format is bespoke. Integrators hand-write mocks.json even though most already own an OpenAPI spec describing the same paths, methods, operation ids, and example responses — plus things mocks.json cannot express (response headers, declared status codes, parameter schemas).
-
Environments are the wrong axis. A running app talks to exactly one base URL at a time (whichever its build points at). Modelling staging and production simultaneously means:
- the UI renders a tab per group×environment, so roughly half the tabs are dead weight for any given build — toggling a mock in the "staging" tab does nothing when the app is built against "prod" (
devview-networkmock/src/commonMain/kotlin/com/worldline/devview/networkmock/NetworkMockScreen.kt:151-165, viewmodel/NetworkMockViewModel.kt:91-104)
environmentId is baked into the DataStore key (EndpointKey.compositeKey, devview-networkmock-core/src/commonMain/kotlin/com/worldline/devview/networkmock/core/model/MockConfiguration.kt:374-387), duplicating persisted state
environmentId is also a response-file directory tier (repository/MockConfigRepository.kt:448-449), duplicating response files on disk
- response-file discovery runs
groups × environments × endpoints (devview-networkmock/src/commonMain/kotlin/com/worldline/devview/networkmock/viewmodel/NetworkMockViewModel.kt:161-183), so the environment axis multiplies an already expensive eager-load loop
The shipped sample proves the axis is being misused for something else: sample/network/src/commonMain/composeResources/files/networkmocks/mocks.json:72-77 uses the prod environment's endpointOverrides to rewrite /api/v1/profile/{userId} → /api/v2/profile/{userId}. That is an API version difference wearing an environment costume. The real requirements are: mocked vs. live (already covered by the existing global toggle) plus API versioning.
Outcome
mocks.json and the environment concept are deleted in a breaking 0.2.0 release. Configuration becomes one OpenAPI 3.x document per API group (JSON or YAML). Operations carry a derived version tag (parsed from the path) for display and filtering only — the engine always responds to whatever version the app actually calls.
Key decisions (apply to every linked issue)
| Decision |
Choice |
| Config format |
OpenAPI 3.x, JSON and YAML. One spec file = one group. info.title → group name, servers[].url → hosts, paths → operations, operationId → operation id, summary → display name. No manifest file — a group spanning multiple API versions is just multiple paths entries in the same spec (e.g. both /api/v1/profile/{id} and /api/v2/profile/{id}). |
| Response bodies |
Stay as external files on disk, referenced from the spec via the standard OpenAPI examples.<name>.externalValue field. No vendor extension needed for this. |
| Versioning |
Version is a derived, display-only tag, parsed from the operation's path using a configurable regex (default /v(\d+)), exposed as a constructor parameter the same way responseSuffixes is today. Version is not part of the operation identity — /api/v1/x and /api/v2/x already have distinct operationIds in the spec, so they are naturally distinct operations. |
| Version behaviour |
Observed only, for 0.2.0. The engine responds to whatever version the app actually calls. A v2 mock can sit unused until the app (or a feature flag) starts calling v2. Forcing/rewriting the app onto a specific version is a deliberately deferred, separate feature (#85 below) because it would mutate real network traffic and needs its own design pass. |
| Parser depth |
Staged. The core parser (JSON + YAML, local #/components refs and external file refs, examples) blocks 0.2.0 since it must fully replace mocks.json. Schema-synthesised response bodies, requestBody matching, and allOf/oneOf/discriminator resolution are follow-up issues in 0.2.x — they add depth but nothing in 0.2.0 depends on them. |
| Migration |
Big-bang breaking change at 0.2.0. No dual-format period — this is a pre-1.0 library (gradle.properties version 0.1.4), so a single clean break is preferable to maintaining two parsers and two DataStore key shapes indefinitely. A migration guide and a conversion script ship alongside the break. |
| Naming |
Adopt OpenAPI vocabulary only for types that model something OpenAPI describes (endpoint → operation, group → spec). Types that model DevView's own runtime mocking behaviour, which OpenAPI has no concept of, keep their current names — see the naming table in #75. |
| Vendor extensions |
Runtime-only behavior vanilla OpenAPI can't express (delay today, failure-rate/sequences as follow-ups) lives under an x-devview object — OpenAPI's standard x--prefixed Specification Extensions mechanism. See #94. |
| Loading strategy |
Discovery returns metadata only (status code + example name) for the main list; response body content loads lazily, only for the operation whose detail screen is actually open. See #98. |
| Format extensibility |
MockConfigRepository (#73) is a pure RawBytes -> ApiSpec seam — no OpenAPI-specific type may leak past it. This keeps a second, lower-ceremony input format (#99) cheap to add later, without deciding to build it now. |
Resulting model shape
OperationKey(specId, operationId) // was EndpointKey(groupId, environmentId, endpointId)
compositeKey = "$specId-$operationId" // DataStore key shape changes -> needs a one-shot prune, see #78
ApiSpec = one OpenAPI document
id <- info.title (slugified)
name <- info.title
servers <- servers[].url, hostnames extracted // replaces EnvironmentConfig.url
Operation
operationId <- operationId
name <- summary
path <- paths key
method <- operation key
version <- regex over path, nullable, display-only
responses <- responses.<code>.content.*.examples.<name>.externalValue
Linked issues
0.2.0 (blocking, in dependency order)
0.2.x follow-ups (OpenAPI depth, not blocking)
Deferred capability (explicitly out of scope for 0.2.0)
Deferred, demand-gated (raised during external design review, tracked deliberately — not scheduled)
Independent of the migration (found during the audit, can land any time)
Suggested order of work
#73 → #74 → #75 → #76 → #98 → #77 → #94 → (#78, #79 in parallel) → (#80, #81 last, since docs follow the API per this repo's documentation-hygiene rule in CLAUDE.md).
#75 (the rename) is sequenced early so that PRs for #76 onward are written directly in the final vocabulary instead of being renamed twice. #98 (lazy loading) lands before #79 (UI rework) so the UI is built against the final loading model once, not reworked twice. #94 (delay vendor extension) depends only on #73 and can move earlier if convenient — it's placed here to keep the "config model settles, then behavior extensions layer on" reading order.
#86–#92 have no dependency on the migration and can be picked up by anyone, at any time, independently. #95–#97 and #99 are the same — deliberately tracked, explicitly not scheduled; #99 additionally requires a demonstrated need before it's picked up at all (see its issue body).
Revision note (2026-07-30)
This epic's scope was revised after discussing the migration with an external AI (Gemini) as a design-partner sounding board. That review's central recommendation — keep the bespoke JSON format as DevView's permanent runtime core, treat OpenAPI as an optional adapter — was considered and rejected: it re-opens the dual-format question already closed in favor of the big-bang break, and its supporting arguments (OpenAPI can't express runtime behavior; in-app overrides need a lightweight model) don't hold up against this codebase specifically, since OperationMockState already separates runtime mock state from parsed config, and the "can't express runtime behavior" gap applies equally to the old bespoke format. No existing issue was closed, reversed, or reduced in scope as a result.
Two real gaps and one good architectural point did survive the review, and are reflected above: the delay-simulation feature needed an explicit home in the new format (#94), whole-config eager loading needed to not carry into a world where specs can be much larger (#98), and the parser-as-a-pure-seam design (already implicit in #73) is now an explicit, checked requirement specifically because it's what keeps a second input format (#99) cheap to add later without deciding to build it now.
Summary
devview-networkmock-corecurrently configures mocks with a bespokemocks.jsonformat built around an API groups × environments model. This epic tracks replacing that with OpenAPI 3.x as the configuration format, and removing the environment axis in favor of a simpler mocked/live split plus API version awareness.This issue is a tracking issue only — no code changes here. Each linked issue is independently implementable and reviewable.
Why
The format is bespoke. Integrators hand-write
mocks.jsoneven though most already own an OpenAPI spec describing the same paths, methods, operation ids, and example responses — plus thingsmocks.jsoncannot express (response headers, declared status codes, parameter schemas).Environments are the wrong axis. A running app talks to exactly one base URL at a time (whichever its build points at). Modelling staging and production simultaneously means:
devview-networkmock/src/commonMain/kotlin/com/worldline/devview/networkmock/NetworkMockScreen.kt:151-165,viewmodel/NetworkMockViewModel.kt:91-104)environmentIdis baked into the DataStore key (EndpointKey.compositeKey,devview-networkmock-core/src/commonMain/kotlin/com/worldline/devview/networkmock/core/model/MockConfiguration.kt:374-387), duplicating persisted stateenvironmentIdis also a response-file directory tier (repository/MockConfigRepository.kt:448-449), duplicating response files on diskgroups × environments × endpoints(devview-networkmock/src/commonMain/kotlin/com/worldline/devview/networkmock/viewmodel/NetworkMockViewModel.kt:161-183), so the environment axis multiplies an already expensive eager-load loopThe shipped sample proves the axis is being misused for something else:
sample/network/src/commonMain/composeResources/files/networkmocks/mocks.json:72-77uses theprodenvironment'sendpointOverridesto rewrite/api/v1/profile/{userId}→/api/v2/profile/{userId}. That is an API version difference wearing an environment costume. The real requirements are: mocked vs. live (already covered by the existing global toggle) plus API versioning.Outcome
mocks.jsonand the environment concept are deleted in a breaking0.2.0release. Configuration becomes one OpenAPI 3.x document per API group (JSON or YAML). Operations carry a derived version tag (parsed from the path) for display and filtering only — the engine always responds to whatever version the app actually calls.Key decisions (apply to every linked issue)
info.title→ group name,servers[].url→ hosts,paths→ operations,operationId→ operation id,summary→ display name. No manifest file — a group spanning multiple API versions is just multiplepathsentries in the same spec (e.g. both/api/v1/profile/{id}and/api/v2/profile/{id}).examples.<name>.externalValuefield. No vendor extension needed for this./v(\d+)), exposed as a constructor parameter the same wayresponseSuffixesis today. Version is not part of the operation identity —/api/v1/xand/api/v2/xalready have distinctoperationIds in the spec, so they are naturally distinct operations.#/componentsrefs and external file refs, examples) blocks 0.2.0 since it must fully replacemocks.json. Schema-synthesised response bodies,requestBodymatching, andallOf/oneOf/discriminator resolution are follow-up issues in 0.2.x — they add depth but nothing in 0.2.0 depends on them.gradle.propertiesversion0.1.4), so a single clean break is preferable to maintaining two parsers and two DataStore key shapes indefinitely. A migration guide and a conversion script ship alongside the break.endpoint→operation,group→spec). Types that model DevView's own runtime mocking behaviour, which OpenAPI has no concept of, keep their current names — see the naming table in #75.x-devviewobject — OpenAPI's standardx--prefixed Specification Extensions mechanism. See #94.MockConfigRepository(#73) is a pureRawBytes -> ApiSpecseam — no OpenAPI-specific type may leak past it. This keeps a second, lower-ceremony input format (#99) cheap to add later, without deciding to build it now.Resulting model shape
Linked issues
0.2.0 (blocking, in dependency order)
feat: OpenAPI 3.x spec loader (JSON + YAML)refactor: remove the environment axisrefactor: adopt OpenAPI vocabulary across the networkmock APIfeat: derive response variants from declared OpenAPI examplesperf: load response bodies lazily instead of eager whole-config discoveryfeat: API version tag with configurable path regexfeat: x-devview vendor extension for delay simulationfix: prune orphaned DataStore keys after the key-shape changefeat: rework the NetworkMock UI for specs and versionsdocs: migrate the sample app and all networkmock docs to OpenAPIdocs: migration guide + mocks.json -> OpenAPI conversion script0.2.x follow-ups (OpenAPI depth, not blocking)
feat: synthesise response bodies from OpenAPI schemasfeat: requestBody matchingfeat: allOf / oneOf / discriminator resolutionDeferred capability (explicitly out of scope for 0.2.0)
feat: force the app onto a given API versionDeferred, demand-gated (raised during external design review, tracked deliberately — not scheduled)
feat: probabilistic failure-rate injectionfeat: stateful / sequential mocksspike: feasibility study for build-time OpenAPI codegenspike/feat: minimal low-ceremony JSON frontend for integrators without a spec— pick up only on demonstrated adoption friction, not speculativelyIndependent of the migration (found during the audit, can land any time)
fix: gate networkmock loggingfeat: serve response headers declared in the specfeat: simulate network failuresrefactor: make MockHttpClientCall internalfix: allow spec reloadtest: cover the gaps left by the auditfix: remove dead preview stubs in NetworkMockEndpointScreenSuggested order of work
#73→#74→#75→#76→#98→#77→#94→ (#78,#79in parallel) → (#80,#81last, since docs follow the API per this repo's documentation-hygiene rule inCLAUDE.md).#75(the rename) is sequenced early so that PRs for#76onward are written directly in the final vocabulary instead of being renamed twice.#98(lazy loading) lands before#79(UI rework) so the UI is built against the final loading model once, not reworked twice.#94(delay vendor extension) depends only on#73and can move earlier if convenient — it's placed here to keep the "config model settles, then behavior extensions layer on" reading order.#86–#92have no dependency on the migration and can be picked up by anyone, at any time, independently.#95–#97and#99are the same — deliberately tracked, explicitly not scheduled;#99additionally requires a demonstrated need before it's picked up at all (see its issue body).Revision note (2026-07-30)
This epic's scope was revised after discussing the migration with an external AI (Gemini) as a design-partner sounding board. That review's central recommendation — keep the bespoke JSON format as DevView's permanent runtime core, treat OpenAPI as an optional adapter — was considered and rejected: it re-opens the dual-format question already closed in favor of the big-bang break, and its supporting arguments (OpenAPI can't express runtime behavior; in-app overrides need a lightweight model) don't hold up against this codebase specifically, since
OperationMockStatealready separates runtime mock state from parsed config, and the "can't express runtime behavior" gap applies equally to the old bespoke format. No existing issue was closed, reversed, or reduced in scope as a result.Two real gaps and one good architectural point did survive the review, and are reflected above: the delay-simulation feature needed an explicit home in the new format (#94), whole-config eager loading needed to not carry into a world where specs can be much larger (#98), and the parser-as-a-pure-seam design (already implicit in #73) is now an explicit, checked requirement specifically because it's what keeps a second input format (#99) cheap to add later without deciding to build it now.