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

108 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: openapi-generator + OpenAPI 3.1 — gotchas генерации клиентов
type: concept
tags: [openapi, openapi-generator, codegen, python, sched, ff, api-contract]
related: [[sched-fnf-r8-verification-stand]]
updated: 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-транспорт + ручная валидация:
```python
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).