7.7 KiB
title, type, tags, related, updated
| title | type | tags | related | updated | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| openapi-generator + OpenAPI 3.1 — gotchas генерации клиентов | concept |
|
|
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): все гочи 1–4 закрыты фиксами
спеки/сервера, 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).
Гочи 1–3 ниже — описание сломанного состояния 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-транспорт + ручная валидация:
Важно: в 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"])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-generator7.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).