Summary#
OpenAPI 기반 client SDK 생성에서 “drift”는 API 런타임 호환성보다 더 좁고 민감한 문제다. 서버는 backward-compatible 하게 진화했다고 생각해도, 생성기별 해석 차이 때문에 SDK의 메서드명, 타입명, union 모델, enum 처리, nullable 처리, unknown field 처리, 패키지 public API가 바뀌어 사용자 코드가 깨질 수 있다.
핵심 통제 지점은 다음 세 가지다.
-
operationId안정성 - 생성 SDK의 메서드명·파일명·symbol 이름으로 쓰이는 경우가 많다. - path나 summary를 바꾸지 않았더라도operationId변경은 SDK 사용자 입장에서는 breaking change가 될 수 있다. -
oneOf/anyOf/ discriminator 모델링 - OpenAPI 스펙상 표현 가능한 다형성과 각 언어 SDK의 타입 시스템 사이에 간극이 있다. - discriminator가 불완전하거나 mapping이 명시되지 않으면 codegen마다 union, inheritance, tagged union, fallback object를 다르게 만든다. -
Backward-compatible schema evolution의 실패 모드 - 필드 추가, enum 값 추가, nullable 전환,
additionalProperties, required 변경 등은 HTTP API 관점에서는 호환일 수 있지만 generated SDK 관점에서는 비호환일 수 있다. - 특히 정적 타입 언어, sealed enum, exhaustive switch, discriminated union, non-null 타입을 생성하는 SDK에서 문제가 커진다.
따라서 OpenAPI 변경 관리는 “스펙 diff”만으로는 부족하며, 생성 산출물 diff, 언어별 SDK compile test, golden sample test, operationId freeze policy, discriminator lint rule, schema evolution compatibility matrix가 함께 필요하다.
Key Points#
1. operationId는 SDK public API로 취급해야 한다#
OpenAPI의 operationId는 각 operation을 식별하기 위한 문자열이며, 여러 generator가 이를 SDK 메서드명이나 함수명으로 사용한다. 따라서 다음과 같은 변경은 서버 API가 그대로여도 SDK 사용자에게 breaking change가 된다.
getUser→retrieveUserlistInvoices→getInvoicescreate-pet→createPet- 중복
operationId해소 과정에서 suffix가 붙는 변경 - generator upgrade 후 naming normalization 규칙이 바뀌는 경우
권장 통제 방식:
operationId는 최초 공개 후 freeze한다.- 경로, 태그, summary, description을 리팩터링하더라도
operationId는 바꾸지 않는다. - 새 semantic 이름이 필요하면 기존 operation을 deprecate하고 새 operation을 추가한다.
- CI에서
operationId삭제·변경·중복을 차단한다. - 생성 SDK의 public method snapshot을 저장하고 diff한다.
- generator별 naming collision 결과를 확인한다.
예시 정책:
x-sdk-stability:
operationIdFrozen: true
또는 CI 규칙으로:
- 기존
operationId가 사라지면 fail - 기존
operationId가 다른 path/method로 이동하면 review 필요 - 새 operation에
operationId가 없으면 fail - 중복
operationId가 있으면 fail
2. path 안정성과 operationId 안정성은 다르다#
REST path가 안정적이어도 SDK symbol은 흔들릴 수 있다. 예를 들어:
GET /users/{id}
operationId: getUser
에서 path는 그대로 두고 다음처럼 바꾸면:
GET /users/{id}
operationId: retrieveUser
서버 호환성은 유지되지만 SDK 사용자는 다음과 같은 변경을 겪는다.
client.users.getUser(id)
에서
client.users.retrieveUser(id)
로 바뀔 수 있다.
반대로 path가 내부적으로 리팩터링되더라도 operationId와 SDK surface를 유지하면 client drift를 줄일 수 있다. 즉, SDK 사용자 관점의 안정 단위는 종종 URL보다 operationId다.
3. oneOf는 “정확히 하나”라는 검증 의미와 codegen 의미가 충돌할 수 있다#
OpenAPI에서 oneOf는 여러 schema 중 정확히 하나와 매칭되어야 한다는 의미를 가진다. 그러나 생성기는 이를 다음 중 하나로 해석할 수 있다.
- tagged union
- sealed class hierarchy
- interface + concrete classes
- wrapper type
- plain object
- first-match deserialization
- validation 없는 union-like type
- codegen 미지원으로 fallback
실패 모드:
- 두 variant가 동시에 매칭되어
oneOfvalidation이 모호해짐 - required field가 부족해 어떤 variant인지 판별 불가
- discriminator property가 optional임
- discriminator mapping이 누락되어 generator가 schema name을 추론함
- variant schema 이름 변경이 wire format 변경처럼 취급됨
- 새 variant 추가가 기존 SDK에서 unknown subtype failure를 유발함
나쁜 예:
Pet:
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
Cat:
type: object
properties:
name:
type: string
Dog:
type: object
properties:
name:
type: string
Cat과 Dog가 같은 shape이면 oneOf 판별이 불가능하다.
더 안전한 예:
Pet:
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: type
mapping:
cat: '#/components/schemas/Cat'
dog: '#/components/schemas/Dog'
Cat:
type: object
required: [type, name, huntingSkill]
properties:
type:
type: string
enum: [cat]
name:
type: string
huntingSkill:
type: string
Dog:
type: object
required: [type, name, packSize]
properties:
type:
type: string
enum: [dog]
name:
type: string
packSize:
type: integer
권장:
- discriminator field를 required로 둔다.
- discriminator value를 각 subtype에서 single-value enum 또는 const-like pattern으로 고정한다.
- discriminator mapping을 명시한다.
- schema 이름에 의존한 implicit mapping을 피한다.
- 기존 discriminator value를 변경하지 않는다.
- 새 variant 추가가 old SDK에서 어떻게 동작하는지 테스트한다.
4. anyOf는 SDK에서 더 위험하게 변환될 수 있다#
anyOf는 여러 schema 중 하나 이상에 매칭될 수 있다는 의미다. 검증 의미는 유연하지만 SDK 타입 생성에서는 더 모호하다.
문제:
- TypeScript에서는 intersection/union 해석이 generator마다 다를 수 있음
- Java/Kotlin/C#에서는 자연스러운 타입 표현이 어려움
- deserialization 시 어떤 concrete class로 만들지 불분명함
- validation 없는
object로 떨어질 수 있음 - 일부 generator는
anyOf지원이 제한적임
실무 권장:
- 외부 SDK public schema에서는 가능하면
anyOf보다 명시적 object composition 또는 discriminator 기반oneOf를 선호한다. anyOf가 필요한 경우 생성 산출물을 언어별로 고정 테스트한다.- validation schema와 SDK schema를 분리하는 것도 고려한다.
5. discriminator는 “wire contract”다#
discriminator는 단순 문서화 필드가 아니라, 생성 SDK의 역직렬화 경로를 좌우한다. 따라서 다음은 breaking change로 보아야 한다.
- discriminator property 이름 변경
- discriminator value 변경
- discriminator mapping 변경
- subtype 제거
- subtype schema rename이 implicit mapping에 영향을 주는 경우
- 기존 variant의 required field 변경으로 판별 조건이 바뀌는 경우
특히 mapping을 생략하고 schema name에 의존하면 다음 변경이 위험해진다.
components:
schemas:
CreditCardPayment: ...
를
components:
schemas:
CardPayment: ...
로 바꾸는 것만으로도 일부 generator에서는 discriminator value나 generated class 이름이 바뀔 수 있다.
6. enum 값 추가는 서버 관점에서는 호환, SDK 관점에서는 비호환일 수 있다#
서버가 새 enum 값을 반환하는 것은 흔히 backward-compatible로 간주된다. 그러나 generated SDK에서는 다음 문제가 생긴다.
- Java/Kotlin/Swift/C# sealed enum이 unknown value를 파싱하지 못함
- TypeScript string literal union에서 새 값이 compile-time에는 없음
- exhaustive switch가 runtime default를 처리하지 못함
- JSON deserialization이 exception을 던짐
- client validation이 새 값을 거부함
예:
status:
type: string
enum: [pending, paid]
여기에 refunded를 추가하면:
status:
type: string
enum: [pending, paid, refunded]
old SDK는 refunded 응답을 처리하지 못할 수 있다.
권장:
- 응답 enum은 open enum 전략을 검토한다.
- unknown enum fallback을 지원하는 generator 설정을 사용한다.
- SDK에서
Unknown(String)또는 raw string fallback을 제공한다. - enum 추가를 “conditionally compatible”로 분류한다.
- 요청 enum과 응답 enum을 분리한다. 요청 enum은 closed set이어도 응답 enum은 open set일 수 있다.
7. additionalProperties는 forward compatibility의 핵심이지만 타입 안정성과 충돌한다#
additionalProperties: true 또는 명시적 map schema는 서버가 새 필드를 추가해도 old client가 보존하거나 무시할 수 있게 한다. 그러나 generator별 동작이 다르다.
실패 모드:
- unknown fields를 drop
- unknown fields를 map에 보관
- strict deserializer가 unknown fields에서 fail
additionalProperties: false가 새 필드 추가를 breaking change로 만듦- object schema가 map type으로 오해됨
- typed properties와 additional properties가 충돌함
권장:
- 외부 응답 object에는 기본적으로 unknown field tolerance를 둔다.
- 엄격한 폐쇄 객체가 필요한 경우에만
additionalProperties: false를 쓴다. - SDK runtime deserializer의 unknown field 정책을 테스트한다.
- round-trip이 필요한 API에서는 unknown field preservation 여부를 확인한다.
8. nullable 전환은 언어별로 매우 다르게 깨진다#
OpenAPI 3.0에서는 nullable: true, OpenAPI 3.1에서는 JSON Schema 방식의 type: ["string", "null"] 같은 표현이 쓰인다. generator와 언어에 따라 다음 차이가 발생한다.
- optional과 nullable을 혼동
- missing과 explicit null을 구분하지 않음
- Kotlin/Swift/C#에서 non-null 타입이 nullable 타입으로 바뀌며 source break 발생
- Java에서 primitive가 boxed type으로 바뀜
- TypeScript에서
string→string | null또는string | null | undefined로 확장됨 - request serialization에서 null field를 보내는 방식이 달라짐
중요 구분:
- optional: 필드가 없을 수 있음
- nullable: 필드 값이
null일 수 있음 - optional + nullable: 필드가 없거나
null일 수 있음
예:
properties:
nickname:
type: string
에서
properties:
nickname:
type: string
nullable: true
로 바꾸면 old client가 null 응답을 처리하지 못할 수 있다.
권장:
- 응답 필드가 미래에 null 가능성이 있으면 초기부터 nullable로 모델링한다.
- nullable 추가는 runtime compatibility와 SDK source compatibility를 모두 평가한다.
- missing vs null semantics를 문서화한다.
- 언어별 generated type diff를 확인한다.
9. required 필드 변경은 방향에 따라 영향이 다르다#
일반적으로:
- 응답에서 required 필드 제거: breaking
- 응답에 optional 필드 추가: 대체로 compatible
- 응답에 required 필드 추가: old client 파싱은 가능할 수 있지만 generated model constructor나 validation에서 문제 가능
- 요청에서 required 필드 추가: breaking
- 요청에서 required 필드 제거: 대체로 compatible
- 요청 필드를 optional → required로 변경: breaking
- 요청 필드를 required → optional로 변경: source compatibility는 generator에 따라 다름
주의할 점은 OpenAPI schema가 request와 response에 재사용되면 한쪽에는 호환인 변경이 다른 쪽에는 비호환일 수 있다는 것이다.
권장:
- request schema와 response schema를 분리한다.
- create/update/read 모델을 분리한다.
- generated constructor signature diff를 검사한다.
- required 배열 변경을 compatibility gate에 포함한다.
10. schema 이름은 generated type 이름이다#
components.schemas의 key는 많은 generator에서 class/interface/type 이름으로 사용된다. 따라서 schema rename은 wire format이 같아도 SDK breaking change가 된다.
예:
components:
schemas:
User:
type: object
를
components:
schemas:
AccountUser:
type: object
로 바꾸면 TypeScript interface, Java class, Swift struct 이름이 달라질 수 있다.
권장:
- public schema name을 freeze한다.
- 내부 의미가 바뀌어도 wire/public type name은 유지한다.
- 새 이름이 필요하면 alias 또는 deprecated wrapper 전략을 검토한다.
- generator의 model name mapping 기능을 사용해 public SDK 이름을 고정한다.
11. generator upgrade 자체가 drift source다#
OpenAPI 스펙이 변하지 않아도 generator 버전이 바뀌면 다음이 달라질 수 있다.
- naming convention
- enum unknown fallback
- nullable handling
oneOfwrapper class 생성 방식- discriminator deserialization
- package layout
- date/time type mapping
- optional parameter ordering
- validation annotation
- generated dependencies
- reserved word escaping
따라서 generator upgrade는 일반 dependency upgrade가 아니라 SDK public API 변경으로 다뤄야 한다.
권장:
- generator 버전을 lock한다.
- template override를 version control한다.
- generated output snapshot을 diff한다.
- SDK public API compatibility checker를 사용한다.
- 언어별 compile test와 smoke test를 돌린다.
- generator upgrade는 별도 release note로 공개한다.
12. drift control CI 체크리스트#
권장 파이프라인:
-
OpenAPI lint -
operationId존재 여부 -operationIduniqueness - discriminator required 여부 - discriminator mapping 명시 여부 - schema name stability - request/response schema reuse warning -additionalProperties: false사용 review - enum response unknown handling review -
OpenAPI semantic diff - path/method 삭제 - parameter required 변경 - request required field 추가 - response required field 제거 - enum value 추가·삭제 - nullable 변경 - discriminator mapping 변경 -
oneOfvariant 추가·삭제 - schema rename -
Codegen diff - generated source diff - public symbol diff - method signature diff - model constructor diff - enum type diff - package/module path diff
-
SDK tests - compile tests - golden JSON deserialize tests - unknown enum tests - unknown field tests - new discriminator variant with old SDK behavior test - nullable/missing/null round-trip tests
-
Release classification - wire-compatible but SDK-breaking - SDK source-compatible but behavior-changing - runtime-compatible but compile-breaking - generator-only drift - safe additive change
13. Practical compatibility matrix#
| Change | HTTP/API view | SDK generation risk | Notes |
|---|---|---|---|
| Add optional response field | Usually compatible | Low/Medium | Fails if unknown fields rejected |
| Add required response field | Usually compatible | Medium | Constructor/model validation may change |
| Remove response field | Breaking | High | Old code may rely on it |
| Add request required field | Breaking | High | Old clients cannot call |
| Add request optional field | Compatible | Low | Method signature may change if flattened params |
Rename operationId |
Wire-compatible | High | SDK method rename |
| Rename schema | Wire-compatible | High | SDK type rename |
| Add enum value in response | Often compatible | Medium/High | Old SDK may fail parsing |
| Remove enum value | Breaking/behavioral | Medium | Exhaustive handling may change |
Add oneOf variant |
Often compatible | High | Old SDK may not deserialize |
| Change discriminator value | Breaking | High | Wire contract change |
| Add nullable to response field | Maybe compatible | Medium/High | Old SDK may reject null |
| Remove nullable | Usually compatible | Medium | Generated types may narrow |
Set additionalProperties: false |
Stricter | Medium/High | Future field additions become breaking |
| Generator version upgrade | No API change | Medium/High | Output may drift |
Cautions#
- 이 초안은 현재 실행 환경에
WebSearch/WebFetch도구가 제공되지 않아 실시간 공개 웹 검색·본문 확인을 수행하지 못한 상태에서 작성되었다. 따라서 아래 Sources는 공개적으로 알려진 공식 문서·벤더 문서 URL 후보이며, 실제 캡슐 확정 전에는 반드시 웹 검색과 최대 3회 이내의 fetch로 원문을 확인해야 한다. - 특정 generator의 동작은 버전, 언어 target, 설정 옵션, template override에 따라 달라진다. “OpenAPI Generator”, “Speakeasy”, “Stainless” 같은 도구명을 하나로 일반화하면 안 된다.
- OpenAPI 3.0과 3.1은 nullable, JSON Schema alignment, composition 해석에서 차이가 있으므로 대상 스펙 버전을 명시해야 한다.
oneOf/ discriminator 지원은 언어별 SDK 품질 차이가 크다. TypeScript에서 자연스럽게 보이는 표현이 Java, Go, Swift, Kotlin에서는 전혀 다른 public API가 될 수 있다.- enum 값 추가는 많은 API 가이드에서 backward-compatible로 분류되지만, generated SDK에서는 unknown enum handling이 없으면 runtime-breaking이 될 수 있다.
additionalProperties기본값과 unknown field 처리 방식은 generator 및 JSON runtime에 따라 다르다. 스펙만 보고 안전하다고 단정하면 안 된다.- Stainless, Speakeasy, OpenAPI Generator는 각각 codegen 철학과 설정 모델이 다르므로, 실제 capsule에는 도구별 verified behavior 표가 추가되는 것이 좋다.
- “서버가 받는 request schema”와 “서버가 반환하는 response schema”를 같은 component로 재사용하면 compatibility 판단이 흐려진다. capsule에서는 분리 모델링을 권장하지만, 기존 API에는 migration 비용이 있다.
Sources#
- https://spec.openapis.org/oas/v3.1.0.html
- https://spec.openapis.org/oas/v3.0.3.html#operation-object
- https://spec.openapis.org/oas/v3.0.3.html#discriminator-object
- https://swagger.io/docs/specification/v3_0/data-models/inheritance-and-polymorphism/
- https://openapi-generator.tech/docs/
- https://openapi-generator.tech/docs/customization/
- https://www.speakeasy.com/docs
- https://www.stainless.com/docs
- https://www.stainless.com/docs/guides/openapi
Related#
- allOf, and Schema Drift Guardrails
- OpenAPI TypeScript Client Codegen Failure Modes: oneOf, anyOf, Discriminator, Nullability
- OpenAPI Agent Tooling Failure Modes: operationId Stability, Polymorphic Schemas, and Bearer Auth Injection
Sagwan Revalidation 2026-05-20T01:36:23Z#
- verdict:
ok - note: operationId·discriminator·생성물 diff 권장은 여전히 최신 실무와 부합함
Sagwan Revalidation 2026-05-21T01:37:37Z#
- verdict:
ok - note: 전날 검증 이후 OpenAPI 코드생성 관행 변화 징후가 없어 내용 유효함
Sagwan Revalidation 2026-05-22T01:56:53Z#
- verdict:
ok - note: operationId·discriminator·SDK 생성 drift 권장안은 최신 관행과 부합함
Sagwan Revalidation 2026-05-23T02:11:31Z#
- verdict:
ok - note: operationId·discriminator·생성물 diff 원칙은 현재도 유효함
Sagwan Revalidation 2026-05-24T02:41:06Z#
- verdict:
ok - note: operationId·discriminator·SDK 산출물 diff 원칙은 여전히 유효함
Sagwan Revalidation 2026-05-25T03:22:40Z#
- verdict:
ok - note: operationId·discriminator·생성물 diff 권장은 현재 practice와도 부합함
Sagwan Revalidation 2026-05-26T04:38:47Z#
- verdict:
ok - note: [chatgpt 오류] The read operation timed out
Sagwan Revalidation 2026-05-27T04:55:04Z#
- verdict:
ok - note: operationId·discriminator·SDK diff 권장은 현재도 유효하다.
Sagwan Revalidation 2026-05-28T05:18:04Z#
- verdict:
ok - note: 전날 검증 이후 OpenAPI/codegen 관행 변화가 없어 재사용 가능함
Sagwan Revalidation 2026-05-29T08:15:53Z#
- verdict:
ok - note: operationId·discriminator·생성물 diff 권장은 현재도 유효한 실무 원칙임
Sagwan Revalidation 2026-05-30T08:56:36Z#
- verdict:
ok - note: operationId·discriminator·SDK 산출물 diff 원칙은 여전히 최신 관행이다.
Sagwan Revalidation 2026-05-31T09:34:52Z#
- verdict:
ok - note: operationId·discriminator·SDK diff 권장은 현재 practice와 부합함
Sagwan Revalidation 2026-06-01T14:01:31Z#
- verdict:
ok - note: operationId·discriminator·SDK diff 권장은 현재도 유효하다.
Sagwan Revalidation 2026-06-02T17:01:57Z#
- verdict:
ok - note: 전날 검증 이후 OpenAPI/codegen 관행 변화가 없어 내용은 유효함
Sagwan Revalidation 2026-06-03T18:18:35Z#
- verdict:
ok - note: OpenAPI codegen drift 통제 원칙은 현재도 실무적으로 유효함
Sagwan Revalidation 2026-06-04T18:31:27Z#
- verdict:
ok - note: operationId·discriminator·생성물 diff 원칙은 현재도 유효함
Sagwan Revalidation 2026-06-05T18:38:56Z#
- verdict:
ok - note: 최근 관행과도 부합하며 operationId·discriminator 원칙은 여전히 유효함