Hand-writing mock API responses feels productive until the backend team renames a field. Your local demo still returns user_name, the contract says username, and the bug report lands on the frontend because the fixture was the only thing that looked wrong. I used to keep these files in sync by hand, usually on a Friday after the spec changed. Now I generate them from the OpenAPI document and let a model do the typing—but only after a schema check rejects bad output.
The reason for the two-step flow is simple: a language model will happily invent a plausible object that violates the contract. It might nest an array where the spec expects a string, or forget a required field. That makes free model access useful only if there is a gate between the model and your mock server. The script below uses MonkeyCode's free model access and free server option; I am treating both as operator-supplied availability claims, not as guaranteed capacity or uptime. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
What the script does
The workflow has three stages:
- Read a response schema from an OpenAPI file.
- Ask the model to produce one JSON object for that schema.
- Validate the object with
jsonschema. If it fails, feed the validator error back to the model and retry, up to three times. If it still fails, fall back to a safe empty example.
This means the model only gets a request for structure, not for meaning. It can guess names and values, but it cannot violate the shape.
Here is the core script. It is written for an OpenAI-compatible chat completions endpoint, which lets you point LLM_BASE_URL at whichever free server you have been given. I have stripped secrets and retry counts from the sample.
import json
import os
import sys
import urllib.request
import jsonschema
import yaml
def load_spec(path):
with open(path, 'r', encoding='utf-8') as f:
if path.endswith('.yaml') or path.endswith('.yml'):
return yaml.safe_load(f)
return json.load(f)
def get_response_schema(spec, path_name, method, status='200'):
operation = spec['paths'][path_name][method.lower()]
response = operation['responses'][str(status)]
content = response.get('content', {})
for media_type, media in content.items():
if 'schema' in media:
return media['schema']
return None
def call_model(base, key, model, prompt):
payload = json.dumps({
'model': model,
'messages': [{'role': 'user', 'content': prompt}],
'temperature': 0.2,
}).encode('utf-8')
req = urllib.request.Request(
base.rstrip('/') + '/chat/completions',
data=payload,
headers={'Authorization': f'Bearer {key}', 'Content-Type': 'application/json'},
method='POST',
)
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.load(resp)
return data['choices'][0]['message']['content'].strip()
def extract_json(text):
# Strip optional markdown fences
if text.startswith('```
'):
text = text.split('
```')[1]
if text.startswith('json'):
text = text[4:]
return json.loads(text.strip())
def build_prompt(schema):
return f'''You are a mock data generator. Return exactly one JSON object that conforms to this JSON Schema. Do not include comments, markdown fences, or explanations.
Schema:
{json.dumps(schema, indent=2)}
'''
def generate_mock(spec_path, path_name, method, status='200'):
spec = load_spec(spec_path)
schema = get_response_schema(spec, path_name, method, status)
if not schema:
raise ValueError('No schema found for that operation')
base = os.environ.get('LLM_BASE_URL')
key = os.environ.get('LLM_API_KEY')
model = os.environ.get('LLM_MODEL', '')
if not base or not key:
raise RuntimeError('Set LLM_BASE_URL and LLM_API_KEY')
validator = jsonschema.Draft7Validator(schema)
last_errors = []
for attempt in range(3):
prompt = build_prompt(schema)
if attempt > 0:
prompt += '\n\nPrevious attempt failed validation. Fix these errors and try again:\n'
prompt += '\n'.join(last_errors)
text = call_model(base, key, model, prompt)
data = extract_json(text)
errors = list(validator.iter_errors(data))
if not errors:
return data
last_errors = [err.message for err in errors[:5]]
# Fallback: build a minimal object from schema defaults
return minimal_example(schema)
def minimal_example(schema):
if 'example' in schema:
return schema['example']
if 'default' in schema:
return schema['default']
if schema.get('type') == 'object':
obj = {}
for prop, subschema in schema.get('properties', {}).items():
obj[prop] = minimal_example(subschema)
return obj
if schema.get('type') == 'array':
return [minimal_example(schema.get('items', {}))] if 'items' in schema else []
if schema.get('type') == 'string':
return ''
if schema.get('type') == 'integer':
return 0
if schema.get('type') == 'number':
return 0.0
if schema.get('type') == 'boolean':
return False
return None
if __name__ == '__main__':
mock = generate_mock(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] if len(sys.argv) > 4 else '200')
print(json.dumps(mock, indent=2))
Run it like this:
python generate_mock.py openapi.yaml /users get 200
The script does not assume a particular model name. You set LLM_BASE_URL to the endpoint you were given and LLM_MODEL to whatever identifier the server accepts.
What the validator catches
The jsonschema check is the actual safety boundary. It catches the mistakes that are cheap to catch and frequent in generated output:
- Missing required properties.
- Wrong types (
nullwhere a string is required). - String too long or missing a pattern.
- Array items that do not match the item schema.
- Enum values that were not in the allowed set.
These are exactly the failures that turn a useful mock into a confusing one. When validation fails, the error messages go back into the prompt, which gives the model a chance to correct only the structure, not the entire sample.
What it does not catch
A valid object can still be semantically useless. If the schema only says string for a field called id, the model may return "abc123" when your frontend expects a UUID. The validator will not complain because the type is correct. You can improve this by adding pattern, format, or maxLength constraints to your OpenAPI schema, but many teams do not maintain schemas at that level of detail.
Other limitations:
- Complex
oneOf,anyOf, and recursive schemas can make the prompt large enough to hit model context limits. - A free server may rate-limit or queue requests, so this should not be part of your production build step.
- The script sends parts of your contract to an external endpoint. Strip internal names or URLs if your spec is not meant to leave your network.
- Date, ID, and relationship consistency across multiple calls is not guaranteed. Generate each mock independently or ask the model for a small seed dataset.
Who should skip this
- Teams that already use a mock server with a schema-aware data generator (for example, a proxy that returns examples from the spec).
- Frontends that need highly realistic, related data, such as a timeline feed with user avatars and consistent timestamps.
- Projects where the OpenAPI spec is incomplete or stale.
- Anyone who cannot send spec fragments to a third-party endpoint.
If your mock API exists only to unblock frontend work between contract changes, this script moves most of the typing away from you without giving up the shape guarantee.
If you have a free model slot, spend it on the boring generation you used to do by hand. You still decide when the output is good enough to serve.
Top comments (0)