Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. The client's preference. Prefer: code=404 picks the status, Prefer: example=lamp a named example, Prefer: dynamic=true generated data (several directives can be combined: Prefer: code=404, dynamic=true). Clients that cannot set Prefer send X-Rustybin-Mock-Status: 404 and X-Rustybin-Mock-Example: lamp. A status or example preference skips the rules and overrides below.
  2. Rules of the operation, in order: the first rule whose conditions all hold answers with its respond.
  3. The operation's respond override: a fixed status, example, body and headers for every call.
  4. The default: the lowest documented 2xx (200 when 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.modeBody 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)
generatedAlways generated from the schema; falls back to the examples only when there is no schema
staticOnly 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.

FieldMeaning
respondThe 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
rulesOrdered list of {name?, when, respond}; the first match wins (20 per operation, 10 in public mode)
modeexamples, generated or static for this operation
delay_msDelay before answering (instead of responses.delay_ms), capped at 30 s (10 s in public mode)
publicNo 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:

KeyValue
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
bodyThe whole body, as text
ConditionHolds when
equals: vThe value equals v, compared as text (13 equals "13")
not_equals: vThe value is missing or differs from v
exists: true / exists: falseThe 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.

ModeEffect
offNothing is checked
warn (default)The mock answers normally, adds X-Rustybin-Mock-Validation: failed and records the issues in the capture's validation
enforce400 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.

LevelExamples
Error (blocks the deploy)A configured body that does not match the schema; an example name that the status does not document
WarningA 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}/sample generates 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}/validate validates a body: {"kind": "response", "status": 200, "body": ...} (the default kind) against the response schema, or {"kind": "request", "body": ...} against the request body schema. It returns valid, has_schema and the issues (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