Files
admin/.wiki/concepts/openapi-generator-3-1-spec-gotchas.md

7.7 KiB
Raw Blame History

title, type, tags, related, updated
title type tags related updated
openapi-generator + OpenAPI 3.1 — gotchas генерации клиентов concept
openapi
openapi-generator
codegen
python
sched
ff
api-contract
sched-fnf-r8-verification-stand
2026-08-18

openapi-generator + OpenAPI 3.1 — gotchas

Проверено вживую на F&F-раундах sched «клиент на любом ЯП одной командой» (2026-08-18): Python-клиент из спеки @sched/admin-api через openapi-generator 7.24.0 (-g python, pydantic v2). Спеку НЕ чинили — обходили (round 1, 0.1.2). Round 2 (0.2.0): все гочи 14 закрыты фиксами спеки/сервера, raw-обход удалён — см. «Статус после round 2» ниже.

Статус после round 2 (0.2.0 / daemon 0.3.5)

Все 5 находок round 1 подтверждены закрытыми (ретест 2026-08-18, отчёт sched/.agents/inbox/2026-08-18T22-55-00Z-admin-openapi-fixes-round2-retest.md):

  • Гоча 1 (wrapped-ответы) — FIXED (спека). Мутации теперь объявляют {task}/{run}/{schedule} (response-компоненты TaskMutation/RunMutation/ ScheduleMutation), GET /runs/{id}?logFromOffset — RunLogChunk (allOf RunRecord + logChunk/logTotalLength). Генератор выдаёт InlineObjectN- модели (поля task/run/schedule/ok), десериализация нативная. raw_ops.py из примера удалён.
  • Гоча 2 (const:true) — FIXED. const убран из спеки; Health200Response.ok = обычный StrictBool.
  • Гоча 3 (oneOf+discriminator) — FIXED. Schedule стал плоской схемой (kind + опциональные cron/timezone/ms/at); rec.schedule.cron читается с типизированной модели. Normalization живьём: once{kind:once, at:ISO}, interval{kind:interval, ms}.
  • F-1 (health auth) — FIXED (сервер). /health всегда открыт, auth — после health-роута; прочие роуты с ключом 401.
  • F-5 (dedupKey fail-fast) — FIXED (сервер). POST/PATCH /schedules с неизвестным top-level полем → 400 с именем поля.
  • Итог ретеста: 11/11 ранее-сырых операций идут через типизированный клиент, сьют 24/24, smoke 19/19 (коммит sched d658ff0).

Гочи 13 ниже — описание сломанного состояния round 1 (0.1.2) + паттерн обхода (потребительский escape-hatch, если спека где-то ещё сырая).

Гоча 1 — ответы {} в спеке ломают десериализацию (главная)

Если операция объявляет ответ {} (пустая схема), а сервер реально отдаёт объект ({task: …}, {run: …}, {schedule: …}, {logChunk, logTotalLength}), сгенерированный клиент валится на pydantic ValidationError — он пытается десериализовать wrapped-тело в объявленную модель, а требуемых полей на верхнем уровне нет (все None).

  • Симптом: ValidationError на 2xx с полями input_value=None.
  • Фикс (у имплементера спеки): объявлять реальные схемы ответов ({task: TaskRecord} и т.п.). Без этого ни один генератор не даёт рабочий клиент.
  • Обход (у потребителя): raw-транспорт + ручная валидация:
    resp = client.call_api("POST", client.configuration.host + "/tasks", body=body)
    data = json.loads(resp.read())          # resp.data может быть None до read()
    rec = TaskRecord.model_validate(data["task"])
    
    Важно: в raw-вызове call_api НЕ делает snake→camel конвертацию запроса — сериализуй через model_dump(exclude_none=True, by_alias=True).

Гоча 2 — const: true компилится в строковый enum

"ok": { "type": "boolean", "const": true } → openapi-generator генерит enum ('true') со СТРОКОВЫМ значением → настоящий true (boolean) не проходит валидацию. Убирать const из схемы или не использовать его на булевых полях.

Гоча 3 — oneOf + discriminator не десериализуется

Schedule (kind: cron|interval|once, oneOf + discriminator) в сгенерированной модели приходит с actual_instance=None — правило (cron) недостижимо через модель, только через raw JSON: raw["schedule"]["schedule"]["cron"]. Касается любого oneOf+discriminator на этой версии генератора (3.1-ветка). Фикс (round 2): плоская output-схема вместо oneOf — kind + опциональные поля; per-kind валидация на сервере (parseScheduleEntry).

Окружение (Windows)

  • openapi-generator 7.x требует Java 11+: системная Java 8 падает на class-load; ставь JAVA_HOME на новый JDK (Temurin 25 работает). Важно (round-2 уточнение): CLI-обёртка (@openapitools/openapi-generator-cli) спавнит java из PATH, не из JAVA_HOME — одного JAVA_HOME мало: $env:Path = "$env:JAVA_HOME\bin;" + $env:Path до запуска, иначе UnsupportedClassVersionError (class file 55.0 vs JRE 52.0 = Java 8 в PATH). Запуск: npx --yes @openapitools/openapi-generator-cli generate -i spec.json -g python -o out --additional-properties=packageName=...
  • Wrapped-ответы, объявленные инлайн в components.responses, дают имена InlineObject..InlineObjectN и доступ resp.schedule.schedule.cron (двойной .schedule). Работает; красивее — именованные схемы в components.schemas.
  • info.version спеки НЕ бьётся вместе с версией пакета (в 0.1.2 и 0.2.0 — 0.1.0); версия живёт только в package.json / dist-tags.
  • Windows excluded port ranges (Hyper-V/WinNAT) резервируют порты (напр. 8090/8091): EACCES на listen. Проверка: netsh interface ipv4 show excludedportrange protocol=tcp.
  • Консоль cp1251: не печатай /«» в скриптах — 'charmap' codec can't encode; ASCII-only.

Паттерн обхода (потребитель, клиент не трогаем) — исторический

Тонкий raw-слой рядом с сгенерированным пакетом (в примере был raw_ops.py): только операции, где спека {}, через call_api; остальное — через типизированный клиент. Сгенерированный пакет остаётся перегенерируемым.

Статус: после round 2 (0.2.0) обход не нужен — raw_ops.py удалён из примера. Паттерн остаётся escape-hatch'ем для сырых спек в других проектах.

Пример целиком: sched/examples/admin-client-python/ (24/24 сьют, 19/19 smoke, typed-only).