openapi-sample-emulator is a lightweight HTTP emulator that serves predefined responses based on an OpenAPI / Swagger specification.
It is designed for local development, integration testing, and CI environments where deterministic and predictable API behavior is required.
- Environment Variables – Configuration options
- Reads an OpenAPI / Swagger specification
- Matches incoming requests by HTTP method and path
- Resolves responses from JSON sample files (folder-based or legacy flat)
- Supports stateful APIs using explicit
scenario.jsondefinitions - Supports step-based and time-based state progression
- Optionally falls back to examples defined in the OpenAPI spec
- Can enforce basic request validation (e.g. required request body)
This tool is useful when:
- You need deterministic, repeatable responses
- You want to test integrations without running real services
- Your API spec has limited or missing examples
- CI tests must be stable and predictable
- You want to simulate long-running or stateful APIs (e.g. scans, jobs, tasks, workflows)
For each request, the emulator resolves responses in the following order:
- Scenario-based responses (
scenario.json, if present) - Folder-based sample files
- Legacy flat sample files (optional)
- OpenAPI response examples (if enabled)
- Otherwise, an error response is returned
The resolution behavior is controlled via LAYOUT_MODE.
The recommended layout mirrors the API path structure:
SAMPLES_DIR/
api/
v1/
items/
GET.json
POST.json
{id}/
GET.json
<path>/<METHOD>[.<state>].json
Examples:
GET /api/v1/items-api/v1/items/GET.jsonPOST /scans-scans/POST.jsonGET /scans/{id}-scans/{id}/GET.json
Path parameters remain as {id}.
Stateful behavior is defined explicitly per endpoint using a scenario.json file placed in that endpoint’s folder.
scans/{id}/status/
scenario.json
GET.requested.json
GET.running.1.json
GET.running.2.json
GET.running.3.json
GET.running.4.json
GET.succeeded.json
Each matching request advances the state by one step.
{
"version": 1,
"mode": "step",
"key": { "pathParam": "id" },
"sequence": [
{ "state": "requested", "file": "GET.requested.json" },
{ "state": "running.1", "file": "GET.running.1.json" },
{ "state": "running.2", "file": "GET.running.2.json" },
{ "state": "running.3", "file": "GET.running.3.json" },
{ "state": "running.4", "file": "GET.running.4.json" },
{ "state": "succeeded", "file": "GET.succeeded.json" }
],
"behavior": {
"advanceOn": [{ "method": "GET" }],
"resetOn": [{ "method": "DELETE", "path": "/scans/{id}" }],
"repeatLast": true
}
}Behavior:
- First
GET-requested - Each subsequent
GETadvances the state - After the last step, the state remains
succeeded(repeatLast: true) DELETE /scans/{id}resets the scenario for thatid
This mode is deterministic and CI-friendly.
If you want the sequence to repeat from the beginning:
"behavior": {
"advanceOn": [{ "method": "GET" }],
"repeatLast": false,
"loop": true
}State progression is based on elapsed seconds since the scenario starts.
{
"version": 1,
"mode": "time",
"key": { "pathParam": "id" },
"timeline": [
{ "afterSec": 0, "state": "requested", "file": "GET.requested.json" },
{ "afterSec": 2, "state": "running.1", "file": "GET.running.1.json" },
{ "afterSec": 7, "state": "succeeded", "file": "GET.succeeded.json" }
],
"behavior": {
"startOn": [{ "method": "GET" }],
"resetOn": [{ "method": "DELETE", "path": "/scans/{id}" }],
"repeatLast": true
}
}Notes:
afterSecmeans “effective from this second onward”.- With
repeatLast: true, once the last milestone is reached it stays there. startOncontrols when the timer starts. If omitted, the timer starts on first access.
If you enable looping:
"behavior": { "loop": true }Make sure the final state is observable for more than an instant.
Recommended pattern: add a “hold” window at the end:
"timeline": [
{ "afterSec": 0, "state": "requested", "file": "GET.requested.json" },
{ "afterSec": 2, "state": "running.1", "file": "GET.running.1.json" },
{ "afterSec": 7, "state": "succeeded", "file": "GET.succeeded.json" },
{ "afterSec": 9, "state": "succeeded", "file": "GET.succeeded.json" }
]This keeps succeeded active for 2 seconds before the loop restarts, which is easier to observe in polling clients.
Time-based mode is useful for demos or UI testing, but may be less suitable for CI due to timing.
For backward compatibility, flat files are still supported:
METHOD__path_with_slashes_replaced_by_underscores.json
Examples:
GET /api/v1/items-GET__api_v1_items.jsonGET /scans/{id}/results-GET__scans_{id}_results.json
Enable via:
LAYOUT_MODE=flat
# or
LAYOUT_MODE=autoLAYOUT_MODE=auto # default: scenario -> folders -> flat
LAYOUT_MODE=folders # only folder-based layout
LAYOUT_MODE=flat # only legacy flat filesOptional request validation can be enabled:
VALIDATION_MODE=requiredCurrently supported:
- Required request body
If the API spec marks a request body as required, requests with an empty body are rejected with HTTP 400.
Supported specs:
- OpenAPI 3.x –
requestBody.required: true - Swagger 2.0 –
in: bodywithrequired: true(via Swagger 2 to OpenAPI 3 conversion using https://github.com/getkin/kin-openapi)
This tool is not intended to:
- Generate random or synthetic data
- Fully validate request schemas
- Replace contract-testing tools
MIT