A well-known problem in developer documentation is code samples becoming stale as the codebase evolves. For my NPS Hikes project, I wanted to explore how to combat this problem with automated checks.
In addition to several new pre-commit hooks, most of this effort comes together in a docs-lint workflow.
Structural checks
Alongside checking code samples, in this workflow:
markdownlintchecks for structurally valid Markdown (blank lines, code fences, headings, and tables, etc).valeenforces semantic style rules.lycheetests that external links are valid.mkdocsruns a strict build of the docs website.
In terms of code samples though, my docs primarily have JSON, bash, and Python examples. I started with static analysis for structural errors.
blacken-docsformats```pythonblocks inside Markdown.rufffeatures in a script that extracts```pythonblocks from Markdown into temporary.pyfiles, and runsruff checkagainst those files.- Another script implements the same pattern for checking the grammar of
```bashcode blocks. - A third does the same for JSON: extract
```jsonblocks from Markdown, sanitize partial examples, and check them withjson.loads().
JSON validation
These tests handle syntax, but don’t directly tackle the problem of stale samples. For that, in the case of JSON in particular, I need to validate JSON samples against the actual OpenAPI schema.
The script validate_json_schema.py validates annotated JSON examples against Pydantic response-model schemas. Instead of a running service, it loads the FastAPI’s app’s OpenAPI schema at check time. Unlike the syntax check in check_json_schema.py, the validation step picks up on renamed or missing fields or incorrect types.
To do this, I map JSON blocks to ParksResponse and TrailsResponse models with an HTML comment.
<!-- response: GET /parks -->
```json
{
"park_count": 3,
...
} ... Given the small number of blocks in my documentation, this was easy. The script:
- Scans Markdown for
<!-- response: METHOD /path -->followed by a```jsonblock. - Maps the endpoint to a Pydantic model via a lookup dict (
GET /parks→ParksResponse). - Generates the JSON schema from the model with
model_json_schema(). - Patches the schema for partial doc examples:
- Removes
requiredconstraints (doc examples intentionally omit fields). - Adds
additionalProperties: false(catches renamed or bogus fields).
- Removes
- Sanitizes the doc JSON (same
.../[...]handling as the syntax checker). - Validates with
jsonschema.Draft202012Validator.
Future improvements
This approach is still fairly limited. For example, it doesn’t catch missing required fields. The required constraint is removed because doc examples are often intentionally partial (e.g., the Trail example omits geometry_type).
While there’s a lot more that could be done for more complex requirements, this approach largely meets the need for the scope of my documentation.