---
title: "Open Library: spec vs live"
date: 2026-10-30
canonical: https://apidrifter.com/research/public-api-spec-drift-2026-11/openlibrary/
part_of: https://apidrifter.com/research/public-api-spec-drift-2026-11/
---

# Open Library: spec vs live

Part of [We checked 13 public APIs against their own OpenAPI specs. 9 had hard mismatches.](https://apidrifter.com/research/public-api-spec-drift-2026-11/).

## Summary

- Spec: https://openlibrary.org/openapi.json (OpenAPI 3)
- Base URL: `https://openlibrary.org`
- Run: 2026-10-30, read-only GET requests, no credentials
- Coverage: 58 operations in the spec, 20 requests sent, 9 successful, 20 operations observed
- Hard violations: 1 distinct schema locations
- Reviewer note: Confirmed: /health returns text/plain, documented as application/json; the spec's own example for search.json spellcheck_count violates its schema. Account endpoints 303-redirect when logged out.

## Hard violations (1)

A typed client generated from the spec would break on these.

- `undocumented-response-media-type` (hard, warning) at `GET /health > response 200 > text/plain`
  - media type "text/plain" is not documented for the 200 response
  - Request: `GET /health -> 200 text/plain`
  - Expected: `["application/json"]`
  - Actual: `"text/plain"`
  - If the spec is the source of truth: Return one of application/json from GET /health.
  - If the implementation is the source of truth: Add "text/plain" under paths["/health"].get.responses["200"].content.

## Undocumented error responses (9)

3xx and 4xx statuses the spec does not mention, usually auth or not-found cases.

- `undocumented-status` (errors, breaking) at `GET /account/loan-history.json > response 303`
  - status 303 is not documented for "GET /account/loan-history.json"
  - Request: `GET /account/loan-history.json?page=1 -> 303 text/html`
  - Expected: `["200","422"]`
  - Actual: `303`
  - If the spec is the source of truth: Return one of the documented statuses (200, 422) from GET /account/loan-history.json.
  - If the implementation is the source of truth: Add a "303" response under paths["/account/loan-history.json"].get.responses.
- `undocumented-status` (errors, breaking) at `GET /account/loans.json > response 303`
  - status 303 is not documented for "GET /account/loans.json"
  - Request: `GET /account/loans.json -> 303 text/html`
  - Expected: `["200"]`
  - Actual: `303`
  - If the spec is the source of truth: Return one of the documented statuses (200) from GET /account/loans.json.
  - If the implementation is the source of truth: Add a "303" response under paths["/account/loans.json"].get.responses.
- `undocumented-status` (errors, breaking) at `GET /import/preview.json > response 401`
  - status 401 is not documented for "GET /import/preview.json"
  - Request: `GET /import/preview.json -> 401 application/json`
  - Expected: `["200","422"]`
  - Actual: `401`
  - If the spec is the source of truth: Return one of the documented statuses (200, 422) from GET /import/preview.json.
  - If the implementation is the source of truth: Add a "401" response under paths["/import/preview.json"].get.responses.
- `undocumented-status` (errors, breaking) at `GET /lists/{olid}/editions.json > response 404`
  - status 404 is not documented for "GET /lists/{olid}/editions.json"
  - Request: `GET /lists/OL1L/editions.json?limit=50&offset=0 -> 404 application/json`
  - Expected: `["200","422"]`
  - Actual: `404`
  - If the spec is the source of truth: Return one of the documented statuses (200, 422) from GET /lists/{olid}/editions.json.
  - If the implementation is the source of truth: Add a "404" response under paths["/lists/{olid}/editions.json"].get.responses.
- `undocumented-status` (errors, breaking) at `GET /lists/{olid}/seeds.json > response 404`
  - status 404 is not documented for "GET /lists/{olid}/seeds.json"
  - Request: `GET /lists/OL1L/seeds.json -> 404 application/json`
  - Expected: `["200","422"]`
  - Actual: `404`
  - If the spec is the source of truth: Return one of the documented statuses (200, 422) from GET /lists/{olid}/seeds.json.
  - If the implementation is the source of truth: Add a "404" response under paths["/lists/{olid}/seeds.json"].get.responses.
- `undocumented-status` (errors, breaking) at `GET /lists/{olid}/subjects.json > response 404`
  - status 404 is not documented for "GET /lists/{olid}/subjects.json"
  - Request: `GET /lists/OL1L/subjects.json?limit=20 -> 404 application/json`
  - Expected: `["200","422"]`
  - Actual: `404`
  - If the spec is the source of truth: Return one of the documented statuses (200, 422) from GET /lists/{olid}/subjects.json.
  - If the implementation is the source of truth: Add a "404" response under paths["/lists/{olid}/subjects.json"].get.responses.
- `undocumented-status` (errors, breaking) at `GET /reading-goal.json > response 401`
  - status 401 is not documented for "GET /reading-goal.json"
  - Request: `GET /reading-goal.json -> 401 application/json`
  - Expected: `["200","422"]`
  - Actual: `401`
  - If the spec is the source of truth: Return one of the documented statuses (200, 422) from GET /reading-goal.json.
  - If the implementation is the source of truth: Add a "401" response under paths["/reading-goal.json"].get.responses.
- `undocumented-status` (errors, breaking) at `GET /search/editions.json > response 404`
  - status 404 is not documented for "GET /search/editions.json"
  - Request: `GET /search/editions.json?fields=cover_i,ebook_access,isbn,key,language,publish_date,publisher,subtitle,title,work_key&limit=100&q=the lord of the rings&sort=&work_key=/works/OL82536W -> 404`
  - Expected: `["200","422"]`
  - Actual: `404`
  - If the spec is the source of truth: Return one of the documented statuses (200, 422) from GET /search/editions.json.
  - If the implementation is the source of truth: Add a "404" response under paths["/search/editions.json"].get.responses.
- `undocumented-status` (errors, breaking) at `GET /series/{olid}/editions.json > response 404`
  - status 404 is not documented for "GET /series/{olid}/editions.json"
  - Request: `GET /series/OL1L/editions.json?limit=50&offset=0 -> 404 application/json`
  - Expected: `["200","422"]`
  - Actual: `404`
  - If the spec is the source of truth: Return one of the documented statuses (200, 422) from GET /series/{olid}/editions.json.
  - If the implementation is the source of truth: Add a "404" response under paths["/series/{olid}/editions.json"].get.responses.

## Our input (4)

Our own request values were wrong. Not the API's fault; listed for honesty.

- `request-accepted-but-invalid` (input, warning) at `GET /search.json`
  - request to "GET /search.json" violated the spec but was accepted with 200
  - Request: `GET /search.json?author=tolkien&author_facet=&author_key=&fields=author_key,author_name,cover_edition_key,cover_height,cover_i,cover_width,ebook_access,edition_count,first_publish_year,has_fulltext,ia,ia_collection,id_cita_press,id_librivox,id_openstax,id_project_gutenberg,id_project_runeberg,id_standard_ebooks,id_wikisource,key,language,lending_edition_s,lending_identifier_s,public_scan_b,series_key,series_name,series_position,subtitle,title&first_publish_year=&language=&limit=100&person_facet=&place_facet=&public_scan_b=&publisher_facet=&q=the lord of the rings&query={"q": "mark"}&spellcheck_count=3&subject_facet=&time_facet=&title=the hobbit -> 200 application/json`
  - Actual: `200`
  - If the spec is the source of truth: Reject requests to GET /search.json that violate the spec; see the request findings for this observation.
  - If the implementation is the source of truth: Loosen the spec for GET /search.json to match what the implementation accepts, if that behavior is intended.
- `request-schema-violation` (input, info) at `GET /search.json > parameter query:spellcheck_count`
  - query parameter spellcheck_count: value matches none of the anyOf alternatives
  - Request: `GET /search.json?author=tolkien&author_facet=&author_key=&fields=author_key,author_name,cover_edition_key,cover_height,cover_i,cover_width,ebook_access,edition_count,first_publish_year,has_fulltext,ia,ia_collection,id_cita_press,id_librivox,id_openstax,id_project_gutenberg,id_project_runeberg,id_standard_ebooks,id_wikisource,key,language,lending_edition_s,lending_identifier_s,public_scan_b,series_key,series_name,series_position,subtitle,title&first_publish_year=&language=&limit=100&person_facet=&place_facet=&public_scan_b=&publisher_facet=&q=the lord of the rings&query={"q": "mark"}&spellcheck_count=3&subject_facet=&time_facet=&title=the hobbit -> 200 application/json`
  - If the spec is the source of truth: Enforce the documented schema in the query parameter spellcheck_count.
  - If the implementation is the source of truth: Adjust the composition at paths["/search.json"].get.parameters[spellcheck_count].schema to describe what is sent.
- `request-constraint-violation` (input, info) at `GET /series/{olid}/seeds.json > parameter path:olid`
  - path parameter olid: value does not match pattern "OL\\d+L"
  - Request: `GET /series/OL45804W/seeds.json -> 422 application/json`
  - Expected: `"OL\\d+L"`
  - Actual: `"OL45804W"`
  - If the spec is the source of truth: Enforce pattern in the path parameter olid.
  - If the implementation is the source of truth: Relax pattern at paths["/series/{olid}/seeds.json"].get.parameters[olid].schema to cover what is sent.
- `request-constraint-violation` (input, info) at `GET /series/{olid}/subjects.json > parameter path:olid`
  - path parameter olid: value does not match pattern "OL\\d+L"
  - Request: `GET /series/OL45804W/subjects.json?limit=20 -> 422 application/json`
  - Expected: `"OL\\d+L"`
  - Actual: `"OL45804W"`
  - If the spec is the source of truth: Enforce pattern in the path parameter olid.
  - If the implementation is the source of truth: Relax pattern at paths["/series/{olid}/subjects.json"].get.parameters[olid].schema to cover what is sent.

## Reproduce

```sh
curl -s -H 'Accept: application/json' 'https://openlibrary.org/health'
```

## Data

- Findings: https://github.com/apidrifter/apidrifter/tree/main/tools/survey/results/openlibrary.findings.json
- Requests sent: https://github.com/apidrifter/apidrifter/tree/main/tools/survey/results/openlibrary.observations.json
- Tool: https://apidrifter.com/docs/cli/ (reproduce with apidrifter run; target definition in tools/survey/targets.json)
