Responses, rules and validation
The examples on this page use a small shop API created like this (two named
examples for 200, one for 404, and a POST /carts with a request body):
POST {{base_url}}/_rustybin/mocks
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{
"name": "docs-shop",
"spec": {
"openapi": "3.0.3",
"info": {"title": "Shop", "version": "1.0"},
"paths": {
"/products/{productId}": {
"get": {
"operationId": "getProduct",
"parameters": [
{"name": "productId", "in": "path", "required": true, "schema": {"type": "integer", "minimum": 1}},
{"name": "currency", "in": "query", "schema": {"type": "string", "enum": ["EUR", "USD"]}}
],
"responses": {
"200": {"description": "A product", "content": {"application/json": {
"schema": {"$ref": "#/components/schemas/Product"},
"examples": {
"mug": {"value": {"productId": 1, "name": "Mug", "price": 9.5}},
"lamp": {"value": {"productId": 2, "name": "Lamp", "price": 49}}
}}}},
"404": {"description": "Unknown product", "content": {"application/json": {
"schema": {"$ref": "#/components/schemas/Error"},
"examples": {"gone": {"value": {"message": "product discontinued"}}}}}}
}
}
},
"/carts": {
"post": {
"operationId": "createCart",
"requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Cart"}}}},
"responses": {
"201": {"description": "Created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Cart"}}}},
"409": {"description": "Coupon refused", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}
}
}
}
},
"components": {"schemas": {
"Product": {"type": "object", "required": ["productId", "name", "price"], "properties": {
"productId": {"type": "integer"}, "name": {"type": "string"}, "price": {"type": "number", "minimum": 0}}},
"Cart": {"type": "object", "required": ["items"], "properties": {
"cartId": {"type": "string", "format": "uuid", "readOnly": true},
"items": {"type": "array", "minItems": 1, "items": {"type": "object", "required": ["productId", "quantity"],
"properties": {"productId": {"type": "integer"}, "quantity": {"type": "integer", "minimum": 1}}}},
"coupon": {"type": "string"}}},
"Error": {"type": "object", "required": ["message"], "properties": {"message": {"type": "string"}}}
}}
}
}
HTTP 201
[Asserts]
jsonpath "$.url" == "{{base_url}}/mock/docs-shop"
jsonpath "$.settings.responses.mode" == "examples"
How a response is chosen
For each request that matched an operation (and passed mocked security
and, in enforce mode, request validation), the first of these that applies wins:
- The client's preference.
Prefer: code=404picks the status,Prefer: example=lampa named example,Prefer: dynamic=truegenerated data (several directives can be combined:Prefer: code=404, dynamic=true). Clients that cannot setPrefersendX-Rustybin-Mock-Status: 404andX-Rustybin-Mock-Example: lamp. A status or example preference skips the rules and overrides below. - Rules of the operation, in order: the first rule whose conditions all hold
answers with its
respond. - The operation's
respondoverride: a fixed status, example, body and headers for every call. - The default: the lowest documented
2xx(200when none is documented), with the body produced by the response mode of the operation (mode) or of the mock (responses.mode).
The status chosen by a preference, rule or override then gets its body: a literal
body if one is configured, else the named example, else the mode. A status the
spec does not document answers an application/problem+json body when it is an
error status (empty otherwise); 204, 304 and 1xx are always empty. Response
headers documented in the spec are sent with values from their examples or
schemas; configured headers are added on top (and replace documented ones with
the same name). The media type follows Accept among the response's content
entries, JSON by default.
X-Rustybin-Mock-Source says what happened: example:<name> (a named example),
example (an unnamed one), generated, static (a configured body), empty, or
undocumented, prefixed with prefer , rule:<name or number> or override
when one of those chose the response.
# Mode "examples": the first named example of the lowest documented 2xx.
GET {{base_url}}/mock/docs-shop/products/5
HTTP 200
[Asserts]
header "X-Rustybin-Mock-Source" == "example:mug"
jsonpath "$.name" == "Mug"
GET {{base_url}}/mock/docs-shop/products/5
Prefer: example=lamp
HTTP 200
[Asserts]
header "X-Rustybin-Mock-Source" == "prefer example:lamp"
jsonpath "$.name" == "Lamp"
GET {{base_url}}/mock/docs-shop/products/5
Prefer: code=404
HTTP 404
[Asserts]
jsonpath "$.message" == "product discontinued"
# The same with headers, for clients that cannot set Prefer.
GET {{base_url}}/mock/docs-shop/products/5
X-Rustybin-Mock-Status: 404
X-Rustybin-Mock-Example: gone
HTTP 404
[Asserts]
header "X-Rustybin-Mock-Source" == "prefer example:gone"
# dynamic=true generates from the schema; productId echoes the path.
GET {{base_url}}/mock/docs-shop/products/5
Prefer: dynamic=true
HTTP 200
[Asserts]
header "X-Rustybin-Mock-Source" == "generated"
jsonpath "$.productId" == 5
jsonpath "$.price" >= 0
GET {{base_url}}/mock/docs-shop/products/5
Prefer: code=418
HTTP 418
[Asserts]
header "Content-Type" == "application/problem+json"
header "X-Rustybin-Mock-Source" == "prefer undocumented"
jsonpath "$.status" == 418
Response modes
responses.mode | Body of a documented response |
|---|---|
examples (default) | The media type's example, else its first named example, else data generated from the schema (schema example values are used where they exist) |
generated | Always generated from the schema; falls back to the examples only when there is no schema |
static | Only bodies you configured (respond.body, example); anything else is empty |
A per-operation mode overrides the mock's. Examples are returned exactly as
written in the spec.
Generated data is deterministic: the same schema, seed and request give the
same body, so a demo or a test is repeatable. Values come from, in order, the
schema's example / examples (in examples mode), const, enum, default,
then plausible data for the type, format (uuid, date, date-time, email, uri,
hostname, ipv4, ipv6, byte, ...), pattern, numeric bounds, string lengths and the
property name (name, email, city, ...). $ref, allOf, oneOf / anyOf
(with discriminator), nullable and 3.1 type arrays, additionalProperties,
minItems / maxItems and uniqueItems are honoured; readOnly properties are
left out of generated requests and writeOnly ones out of responses.
responses.seed (default 1) changes the data; the sample endpoint takes
?seed= to preview another seed.
Top-level properties named like a path parameter echo the request's value when
it fits the schema: GET /products/5 generates "productId": 5, and with exactly
one ...id parameter a property called id gets it too.
# PATCH replaces whole sections: unspecified response settings take their
# defaults. "deploy": true publishes the draft in the same call.
PATCH {{base_url}}/_rustybin/mocks/docs-shop
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{"responses": {"mode": "generated", "seed": 7}, "deploy": true}
HTTP 200
[Asserts]
jsonpath "$.settings.responses.mode" == "generated"
jsonpath "$.settings.responses.validate_requests" == "warn"
jsonpath "$.has_draft_changes" == false
GET {{base_url}}/mock/docs-shop/products/5
HTTP 200
[Asserts]
header "X-Rustybin-Mock-Source" == "generated"
jsonpath "$.productId" == 5
[Captures]
first_name: jsonpath "$.name"
# Same seed, same request, same body.
GET {{base_url}}/mock/docs-shop/products/5
HTTP 200
[Asserts]
jsonpath "$.name" == "{{first_name}}"
PATCH {{base_url}}/_rustybin/mocks/docs-shop
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{"responses": {"mode": "static"}, "deploy": true}
HTTP 200
GET {{base_url}}/mock/docs-shop/products/5
HTTP 200
[Asserts]
header "X-Rustybin-Mock-Source" == "empty"
bytes count == 0
# Back to the defaults (mode examples).
PATCH {{base_url}}/_rustybin/mocks/docs-shop
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{"responses": {}, "deploy": true}
HTTP 200
[Asserts]
jsonpath "$.settings.responses.mode" == "examples"
Operation settings
PUT /_rustybin/mocks/{name}/operations/{op} replaces one operation's settings in
the draft (null or {} clears them); deploy to make them live. The same object
sits under operations.<operationId> in PATCH bodies and in
rustybin.yaml.
| Field | Meaning |
|---|---|
respond | The operation's override: status, example (a named example of that status), body (JSON is serialised; a string is sent as is, labelled JSON when it parses as JSON, else text) and headers |
rules | Ordered list of {name?, when, respond}; the first match wins (20 per operation, 10 in public mode) |
mode | examples, generated or static for this operation |
delay_ms | Delay before answering (instead of responses.delay_ms), capped at 30 s (10 s in public mode) |
public | No mocked security on this operation |
GET /_rustybin/mocks/{name}/operations/{op} returns the operation with its
schemas inlined (what the console's editor builds its forms from), its settings,
default status, effective mode, check result, a sample request and its URL.
PUT {{base_url}}/_rustybin/mocks/docs-shop/operations/getProduct
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{
"rules": [
{
"name": "discontinued",
"when": {"path.productId": {"equals": 13}},
"respond": {"status": 404, "example": "gone"}
},
{
"name": "gold customers",
"when": {"header.x-tier": {"equals": "gold"}, "query.currency": {"exists": true}},
"respond": {"status": 200, "body": {"productId": 7, "name": "Gold lamp", "price": 0}, "headers": {"X-Discount": "100%"}}
}
],
"respond": {"example": "lamp"},
"delay_ms": 100
}
HTTP 200
[Asserts]
jsonpath "$.settings.rules" count == 2
jsonpath "$.report.status" == "ok"
POST {{base_url}}/_rustybin/mocks/docs-shop/deploy
Authorization: Bearer {{admin_token}}
HTTP 200
GET {{base_url}}/_rustybin/mocks/docs-shop/operations/getProduct
HTTP 200
[Asserts]
jsonpath "$.operation.method" == "GET"
jsonpath "$.operation.path" == "/products/{productId}"
jsonpath "$.default_status" == 200
jsonpath "$.mode" == "examples"
jsonpath "$.settings.delay_ms" == 100
jsonpath "$.sample_request.method" == "GET"
jsonpath "$.url" startsWith "{{base_url}}/mock/docs-shop/products/"
Rules
when maps request values to conditions; every condition must hold:
| Key | Value |
|---|---|
path.<name> | A path parameter |
query.<name> | A query parameter (the first one with that name) |
header.<name> | A request header (case-insensitive name) |
body.<dotted.path> | A value in a JSON body: body.coupon, body.items.0.quantity |
body | The whole body, as text |
| Condition | Holds when |
|---|---|
equals: v | The value equals v, compared as text (13 equals "13") |
not_equals: v | The value is missing or differs from v |
exists: true / exists: false | The value is present / absent |
matches: "regex" | The value matches the regular expression (up to 256 characters) |
Several conditions in one entry must all hold ({equals: "a", exists: true}).
The rule's respond works like the operation's override; without status the
default status is used, without body or example the body comes from the mode.
Rules that can never match (an unknown path parameter, a body rule on an operation
without a request body) are reported as warnings by the deploy checks.
GET {{base_url}}/mock/docs-shop/products/13
HTTP 404
[Asserts]
header "X-Rustybin-Mock-Source" == "rule:discontinued example:gone"
jsonpath "$.message" == "product discontinued"
duration >= 100
GET {{base_url}}/mock/docs-shop/products/5?currency=EUR
X-Tier: gold
HTTP 200
[Asserts]
header "X-Discount" == "100%"
header "X-Rustybin-Mock-Source" == "rule:gold customers static"
jsonpath "$.name" == "Gold lamp"
# No rule matches: the operation's respond override.
GET {{base_url}}/mock/docs-shop/products/5
HTTP 200
[Asserts]
header "X-Rustybin-Mock-Source" == "override example:lamp"
jsonpath "$.name" == "Lamp"
# Prefer wins over rules and overrides.
GET {{base_url}}/mock/docs-shop/products/13
Prefer: code=200
HTTP 200
[Asserts]
header "X-Rustybin-Mock-Source" == "prefer example:mug"
PUT {{base_url}}/_rustybin/mocks/docs-shop/operations/createCart
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{
"rules": [
{
"when": {"body.coupon": {"matches": "^FREE"}},
"respond": {"status": 409, "body": {"message": "coupon refused"}}
},
{
"when": {"body.items.0.quantity": {"not_equals": 1}, "header.x-dry-run": {"exists": true}},
"respond": {"status": 201, "headers": {"X-Dry-Run": "true"}}
}
]
}
HTTP 200
POST {{base_url}}/_rustybin/mocks/docs-shop/deploy
Authorization: Bearer {{admin_token}}
HTTP 200
POST {{base_url}}/mock/docs-shop/carts
Content-Type: application/json
{"items": [{"productId": 1, "quantity": 2}], "coupon": "FREE-LUNCH"}
HTTP 409
[Asserts]
header "X-Rustybin-Mock-Source" == "rule:1 static"
jsonpath "$.message" == "coupon refused"
POST {{base_url}}/mock/docs-shop/carts
Content-Type: application/json
X-Dry-Run: yes
{"items": [{"productId": 1, "quantity": 2}]}
HTTP 201
[Asserts]
header "X-Dry-Run" == "true"
header "X-Rustybin-Mock-Source" == "rule:2 generated"
jsonpath "$.cartId" isUuid
jsonpath "$.items" count >= 1
Request validation
responses.validate_requests decides what happens to requests that do not match
the spec. Path, query, header and cookie parameters are checked (text is coerced to
the schema type first, so ?limit=5 is an integer), as are required parameters,
the request body's presence, content type, JSON syntax and schema.
| Mode | Effect |
|---|---|
off | Nothing is checked |
warn (default) | The mock answers normally, adds X-Rustybin-Mock-Validation: failed and records the issues in the capture's validation |
enforce | 400 application/problem+json with an errors list of {pointer, message}, nothing else is answered |
Pointers address a virtual document: /path/productId, /query/limit,
/header/x-request-id, /body/items/0/quantity.
# Default "warn": the mock answers, flags the request and records the issues.
POST {{base_url}}/mock/docs-shop/carts
Content-Type: application/json
{"items": [], "coupon": 42}
HTTP 201
[Asserts]
header "X-Rustybin-Mock-Validation" == "failed"
GET {{base_url}}/_rustybin/mocks/docs-shop/requests?limit=1
HTTP 200
[Asserts]
jsonpath "$.requests[0].validation[*].pointer" contains "/body/items"
jsonpath "$.requests[0].validation[*].pointer" contains "/body/coupon"
PATCH {{base_url}}/_rustybin/mocks/docs-shop
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{"responses": {"validate_requests": "enforce"}, "deploy": true}
HTTP 200
POST {{base_url}}/mock/docs-shop/carts
Content-Type: application/json
{"items": [], "coupon": 42}
HTTP 400
[Asserts]
header "Content-Type" == "application/problem+json"
jsonpath "$.detail" == "the request does not match the OpenAPI description"
jsonpath "$.operation" == "createCart"
jsonpath "$.errors[*].pointer" contains "/body/coupon"
jsonpath "$.errors[*].pointer" contains "/body/items"
jsonpath "$.errors[*].message" contains "expected string, got integer 42"
GET {{base_url}}/mock/docs-shop/products/abc
HTTP 400
[Asserts]
jsonpath "$.errors[0].pointer" == "/path/productId"
# Restore the default (warn).
PATCH {{base_url}}/_rustybin/mocks/docs-shop
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{"responses": {"validate_requests": "warn"}, "deploy": true}
HTTP 200
Deploy checks
Every draft is checked when it is created or edited, and a deploy is refused
(422, the report in the body) while the checks report errors (unless
?force=true). For each operation the checks generate a valid request
(required parameters, a body, demo credentials when security is on) and render
every response the mock can send (the default, each rule and override, each
documented status), validating each body against its schema.
| Level | Examples |
|---|---|
| Error (blocks the deploy) | A configured body that does not match the schema; an example name that the status does not document |
| Warning | A spec example or generated data that does not match its schema; a rule on an unknown path or query parameter or on a body the operation does not have; settings for an operationId the spec does not have; security on in spec mode while the spec declares none; every parse warning (undeclared path parameters, operations without responses, unsupported keywords, ...) |
# A custom body that breaks the schema is a deploy error.
PUT {{base_url}}/_rustybin/mocks/docs-shop/operations/getProduct
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{"respond": {"body": {"productId": "seven"}}}
HTTP 200
[Asserts]
jsonpath "$.report.status" == "error"
POST {{base_url}}/_rustybin/mocks/docs-shop/deploy
Authorization: Bearer {{admin_token}}
HTTP 422
[Asserts]
jsonpath "$.error" contains "deploy checks found 1 error"
jsonpath "$.report.ok" == false
jsonpath "$.report.errors" == 1
jsonpath "$.report.operations[0].id" == "getProduct"
jsonpath "$.report.operations[0].status" == "error"
jsonpath "$.report.operations[0].findings[0].message" contains "response does not match the schema"
# The live revision still answers; clear the operation settings instead.
PUT {{base_url}}/_rustybin/mocks/docs-shop/operations/getProduct
Authorization: Bearer {{admin_token}}
Content-Type: application/json
null
HTTP 200
[Asserts]
jsonpath "$.report.status" == "ok"
GET /_rustybin/mocks/{name}/report returns the full report of the draft: ok,
errors, warnings, spec_warnings, mock-level findings, and per operation its
status (ok, warning, error), findings, the sample request and each
checked response (source, status, body_source, valid, issues). The
console's Deploy report shows it with a curl command per request.
GET {{base_url}}/_rustybin/mocks/docs-shop/report
HTTP 200
[Asserts]
jsonpath "$.ok" == true
jsonpath "$.errors" == 0
jsonpath "$.operations[*].id" contains "createCart"
jsonpath "$.operations[0].id" == "getProduct"
jsonpath "$.operations[0].status" == "ok"
jsonpath "$.operations[0].request.path" matches /^\/products\/\d+$/
jsonpath "$.operations[0].responses[*].status" contains 404
jsonpath "$.operations[1].request.body.items" isCollection
jsonpath "$.operations[1].responses[*].source" contains "rule 1"
Samples and validation
Two read-only helpers power the console's editor and "Try it", and are handy in scripts:
GET .../operations/{op}/samplegenerates a valid request and the response the mock would send, validated:?status=picks a documented status,?mode=(examples,generated,static),?example=a named example and?seed=a seed.POST .../operations/{op}/validatevalidates a body:{"kind": "response", "status": 200, "body": ...}(the default kind) against the response schema, or{"kind": "request", "body": ...}against the request body schema. It returnsvalid,has_schemaand theissues(pointers relative to the body).
GET {{base_url}}/_rustybin/mocks/docs-shop/operations/getProduct/sample?mode=generated&seed=3
HTTP 200
[Asserts]
jsonpath "$.request.method" == "GET"
jsonpath "$.url" startsWith "{{base_url}}/mock/docs-shop/products/"
jsonpath "$.response.status" == 200
jsonpath "$.response.source" == "generated"
jsonpath "$.response.valid" == true
jsonpath "$.response.body.price" isNumber
GET {{base_url}}/_rustybin/mocks/docs-shop/operations/getProduct/sample?status=404
HTTP 200
[Asserts]
jsonpath "$.response.status" == 404
jsonpath "$.response.source" == "example:gone"
POST {{base_url}}/_rustybin/mocks/docs-shop/operations/getProduct/validate
Content-Type: application/json
{"status": 200, "body": {"productId": "seven", "name": "Mug"}}
HTTP 200
[Asserts]
jsonpath "$.valid" == false
jsonpath "$.issues[*].pointer" contains "/productId"
jsonpath "$.issues[*].message" contains "expected integer, got string \"seven\""
jsonpath "$.issues[*].message" contains "missing required property \"price\""
POST {{base_url}}/_rustybin/mocks/docs-shop/operations/createCart/validate
Content-Type: application/json
{"kind": "request", "body": {"items": [{"productId": 1, "quantity": 1}]}}
HTTP 200
[Asserts]
jsonpath "$.valid" == true