Split View: 2026 API 스키마 지도 — JSON Schema · OpenAPI 3.1 · AsyncAPI · GraphQL · gRPC · Smithy · TypeSpec 한 번에 정리
2026 API 스키마 지도 — JSON Schema · OpenAPI 3.1 · AsyncAPI · GraphQL · gRPC · Smithy · TypeSpec 한 번에 정리
프롤로그 — 스키마는 이제 코드보다 먼저 온다
2026년의 백엔드 회의는 이렇게 시작한다. "엔드포인트부터 짜지 말고, 스키마부터 보여줘."
핸들러를 먼저 쓰고 그 결과를 OpenAPI로 뽑던 시절이 있었다. spec-from-code — 코드가 진실, 스펙은 부산물. 편했다. 그리고 망가졌다. 두 팀이 서로의 응답 모양을 추측하다가 한 달짜리 통합 버그를 만들고, 클라이언트 SDK는 항상 한 박자 늦었고, 사내 위키의 "현재 API" 문서는 절반이 거짓말이었다.
2026년은 정반대다. spec-first — 스키마를 먼저 쓰고, 거기서 타입·문서·서버 스텁·클라이언트 SDK·테스트 픽스처·AI 도구 정의까지 생성한다. 진실은 스키마 파일에 있다.
그래서 한 번 정리하자. 같은 "API"라도 프로토콜에 따라 도구가 다르다. REST는 OpenAPI 3.1, 이벤트는 AsyncAPI 3.0, 서비스 간은 Protobuf, 프런트엔드 친화는 GraphQL, 그리고 그 모든 것의 기초 문법은 JSON Schema 2020-12. 거기에 AWS의 Smithy, Microsoft의 TypeSpec, 계약 테스트의 Pact — 이름은 많지만 역할은 또렷이 나뉜다.
이 글은 2026년의 API 스키마 지형도다. 각 포맷의 정체와 강점, 같은 도메인 모델을 네 가지 포맷으로 옮겨본 비교, AI 도구 호출에서 JSON Schema가 차지한 자리, 그리고 "우리 팀은 무엇을 써야 하나"에 대한 정직한 의사결정 프레임까지.
1장 · JSON Schema 2020-12 — 모든 것의 토대
먼저 한 줄로: JSON Schema는 "JSON 값의 모양을 또 다른 JSON으로 묘사하는" 표준이다. "이 필드는 정수, 이건 1~120 사이, 이건 ISO 8601 날짜" — 그런 제약을 선언으로 적는다.
draft 2020-12가 사실상 안정 베이스라인이다. 그 이후 작업은 진행 중이지만, 2026년 현재 호환성·도구 지원이 가장 두꺼운 버전이 2020-12다. OpenAPI 3.1이 이 draft에 완전히 정렬되면서 둘 사이의 미세한 충돌(예전에 nullable, example 차이로 골치 아팠던 문제들)이 거의 사라졌다.
JSON Schema가 2026년에 왜 중요해졌나? 세 가지 자리에서 동시에 쓰이기 때문이다.
- REST API: OpenAPI 3.1의
components.schemas안은 그대로 JSON Schema 2020-12다. - AI 도구 호출(function calling): OpenAI·Anthropic·Gemini의 도구 정의 포맷이 전부 JSON Schema 서브셋이다. 모델이
arguments를 JSON으로 채워 반환하면, 호스트가 같은 스키마로 검증한다. - 설정 검증: kubeconfig·CI 워크플로·앱 설정 파일을 검증할 때 가장 흔하게 만나는 표준.
JSON Schema는 "포맷"이 아니라 문법이다. 한 번 배우면 위 세 자리 모두에서 같은 문법을 쓴다. 2026년의 백엔드 엔지니어가 가장 자주 손대게 되는 단일 표준이라고 해도 과언이 아니다.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/user.json",
"type": "object",
"required": ["id", "email"],
"properties": {
"id": { "type": "string", "format": "uuid" },
"email": { "type": "string", "format": "email" },
"age": { "type": "integer", "minimum": 0, "maximum": 130 },
"roles": {
"type": "array",
"items": { "type": "string", "enum": ["admin", "member", "guest"] },
"uniqueItems": true
}
},
"additionalProperties": false
}
이 작은 파일 하나가 OpenAPI 안에도 들어가고, AI 도구 정의에도 들어가고, 단독 validator에도 들어간다. 재사용 가능한 단위가 곧 진실이다.
2장 · OpenAPI 3.1 — REST의 표준은 끝났다, 그리고 시작됐다
OpenAPI 3.0과 3.1의 핵심 차이는 한 줄이다. 3.1은 JSON Schema 2020-12에 완전히 정렬됐다. 3.0 시대의 nullable: true는 사라지고 "type": ["string", "null"]이 표준이다. example/examples의 의미가 정리됐다. $ref가 더 자유로워졌다 — JSON Schema 어디든 가리킬 수 있다.
2026년에 OpenAPI 3.1은 REST API 스펙의 사실상 표준이다. Swagger 시대의 흔적은 거의 사라졌고, 새 프로젝트는 거의 모두 3.1로 시작한다. OpenAPI 3.2 작업이 진행되고 있지만(폼·헤더·다중 응답 콘텐츠 같은 작은 표면을 다듬는 중), 3.1과 3.2는 깨는 변경 없이 진화하는 관계다. 지금 3.1로 시작해도 안전하다.
openapi: 3.1.0
info:
title: Orders API
version: 1.0.0
paths:
/users/{userId}/orders:
get:
summary: List orders for a user
parameters:
- name: userId
in: path
required: true
schema: { type: string, format: uuid }
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Order'
components:
schemas:
Order:
type: object
required: [id, userId, total]
properties:
id: { type: string, format: uuid }
userId:{ type: string, format: uuid }
total: { type: number, minimum: 0 }
status:
type: string
enum: [pending, paid, shipped, cancelled]
이 한 파일에서 생성되는 것들: TypeScript/Go/Python 클라이언트 SDK, 서버 스텁, Postman/Bruno 컬렉션, 인터랙티브 문서, mock 서버, 계약 테스트 픽스처, AI 에이전트가 호출할 도구 정의. 이게 2026년의 워크플로다 — 한 스펙에서 모든 산출물.
Swagger UI는 여전히 동작하지만, 2026년의 새 표준은 Stoplight Elements와 Scalar다. 둘 다 OpenAPI 3.1을 네이티브로 다루고, 다크모드·검색·요청 시도(try-it)·코드 샘플을 훨씬 깔끔하게 보여준다. Scalar는 특히 <script> 한 줄로 끝나는 가벼운 임베드로 빠르게 퍼졌다. Swagger UI는 "있어도 되는" 옵션이지, 더 이상 기본값이 아니다.
3장 · AsyncAPI 3.0 — 이벤트의 OpenAPI
REST는 OpenAPI가 있다. 그럼 카프카 토픽, MQTT 채널, WebSocket 스트림, Server-Sent Events는? 2026년의 답은 분명하다 — AsyncAPI 3.0.
AsyncAPI 2.x 시절의 가장 큰 비판은 "채널 = 토픽 = 동작 한 묶음"이라는 모호함이었다. 발행과 구독이 한 채널 정의 안에서 섞였다. 3.0은 이를 operations로 분리했다. 채널은 메시지가 흐르는 길이고, operation은 발행/수신이라는 행위다. 이 분리만으로도 카프카 같은 시스템을 모델링하기가 훨씬 깔끔해졌다.
AsyncAPI 3.0의 핵심 단어들:
- channels — 메시지가 흐르는 경로(토픽·큐·채널).
- operations — 그 채널에서 일어나는 행위(send·receive). 명시적이다.
- messages — 메시지의 페이로드 스키마(여기서 JSON Schema 그대로 재사용).
- servers — Kafka 브로커, MQTT 브로커, WebSocket 엔드포인트 등 프로토콜별 바인딩.
asyncapi: 3.0.0
info:
title: Orders Events
version: 1.0.0
servers:
kafka-prod:
host: kafka.prod.example.com:9092
protocol: kafka
channels:
orderCreated:
address: orders.created.v1
messages:
OrderCreated:
$ref: '#/components/messages/OrderCreated'
operations:
publishOrderCreated:
action: send
channel: { $ref: '#/channels/orderCreated' }
components:
messages:
OrderCreated:
payload:
type: object
required: [orderId, userId, createdAt]
properties:
orderId: { type: string, format: uuid }
userId: { type: string, format: uuid }
createdAt: { type: string, format: date-time }
total: { type: number, minimum: 0 }
AsyncAPI 3.0이 2026년에 자리잡은 이유는 두 가지다. 첫째, 이벤트 주도 시스템이 더 이상 "특수 케이스"가 아니다. 거의 모든 마이크로서비스 회사가 카프카·NATS·MQTT 중 하나를 쓴다. 둘째, AsyncAPI 생태계가 OpenAPI 흉내를 넘어 자기 도구를 갖췄다 — Studio, generator(서버 스텁·클라이언트), validator, 카탈로그. 사용성 격차가 거의 사라졌다.
REST와 이벤트가 같은 시스템에 섞여 있는 게 흔한 2026년에는, OpenAPI와 AsyncAPI를 한 저장소에서 함께 관리하는 패턴이 표준이다. 둘 다 JSON Schema를 기반으로 하니 메시지 페이로드를 공유하기도 쉽다.
4장 · GraphQL SDL — 프런트엔드와의 평화 협정
GraphQL은 2015년 페이스북이 발표했고, 2019년 GraphQL Foundation으로 옮겨갔으며, 2026년에도 여전히 강력한 영역을 갖고 있다. 특히 여러 클라이언트가 다른 모양의 데이터를 필요로 하는 곳 — BFF, 모바일 앱이 여러 종류 있는 콘텐츠 플랫폼, 대시보드.
GraphQL의 스키마는 **SDL(Schema Definition Language)**이라는 자체 DSL로 쓴다. OpenAPI/AsyncAPI/JSON Schema와 달리 JSON이 아니다.
type User {
id: ID!
email: String!
age: Int
orders(status: OrderStatus, limit: Int = 20): [Order!]!
}
type Order {
id: ID!
total: Float!
status: OrderStatus!
createdAt: DateTime!
}
enum OrderStatus {
PENDING
PAID
SHIPPED
CANCELLED
}
type Query {
user(id: ID!): User
orders(userId: ID!, status: OrderStatus): [Order!]!
}
scalar DateTime
GraphQL의 매력은 introspection — 스키마 자체가 API에서 쿼리 가능하다는 점이다. 클라이언트 도구는 라이브 서버에 붙어 스키마를 끌어오고, 코드젠을 돌려 타입 안전한 훅을 만든다. Apollo Studio, GraphiQL, GraphQL Code Generator, urql, Relay — 도구 생태계는 두껍다.
2026년의 GraphQL 트렌드:
- Federation v2가 정착했다 — 여러 서비스의 GraphQL을 하나의 슈퍼그래프로 합치는 표준.
- Persisted queries가 보안·성능의 기본값이 됐다 — 임의의 쿼리를 서버에 보내지 않고, 빌드 타임에 등록된 쿼리 해시만 보낸다.
- REST와 공존한다. "전부 GraphQL로 갈아엎자"는 흐름은 사라졌고, "프런트엔드가 자주 변하는 곳만 GraphQL, 백엔드 간은 gRPC/REST"가 흔한 패턴이다.
GraphQL의 약점도 정직하게 보자. 캐싱이 까다롭다(URL이 하나라 HTTP 캐싱이 단순하지 않다). N+1 문제는 DataLoader 같은 패턴으로 풀어야 한다. 쿼리 비용 분석을 하지 않으면 클라이언트 한 명이 서버를 무너뜨릴 수 있다. 그리고 모든 곳에 맞지는 않는다 — CRUD 위주의 단순한 API라면 OpenAPI 쪽이 거의 항상 단순하다.
5장 · gRPC + Protobuf — 서비스 간의 기본값
서비스 간 호출이라면 2026년에도 여전히 gRPC + Protobuf가 기본값이다. 이유는 단순하다 — 작고, 빠르고, 다국어이고, 스트리밍이 일급이다.
Protobuf는 바이너리 스키마다. .proto 파일에 메시지·서비스를 정의하면, protoc(또는 더 흔히 Buf)가 언어별 코드를 만든다. 와이어 위에서는 JSON보다 훨씬 작고(필드 이름이 정수 태그로 압축), 파싱이 훨씬 빠르다.
syntax = "proto3";
package shop.v1;
import "google/protobuf/timestamp.proto";
message User {
string id = 1;
string email = 2;
int32 age = 3;
repeated string roles = 4;
}
message Order {
string id = 1;
string user_id = 2;
double total = 3;
OrderStatus status = 4;
google.protobuf.Timestamp created_at = 5;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_PAID = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_CANCELLED = 4;
}
service OrdersService {
rpc GetOrders(GetOrdersRequest) returns (GetOrdersResponse);
rpc StreamOrders(stream GetOrdersRequest) returns (stream Order);
}
message GetOrdersRequest { string user_id = 1; }
message GetOrdersResponse { repeated Order orders = 1; }
2026년 Protobuf 생태계의 중심은 거의 Buf다. buf build, buf lint, buf breaking, buf generate — protoc의 사용성 문제를 모두 해결한 도구 모음. Buf Schema Registry는 protobuf 스키마의 깃허브 역할을 한다 — 버전·릴리스·종속성을 관리한다.
언제 gRPC인가:
- 서비스 간 호출 — 마이크로서비스 내부 망.
- 양방향 스트리밍 — 채팅, 라이브 데이터, 게임 서버.
- 고성능이 정말 중요한 곳 — RPS가 매우 높거나 페이로드가 크거나.
언제 gRPC가 아닌가:
- 브라우저에서 직접 호출 — gRPC-Web이 있지만 여전히 번거롭다. 차라리 REST/GraphQL.
- 3자 공개 API — 외부 개발자가 쓰기엔 진입 장벽이 높다.
- 디버깅을 평문으로 하고 싶을 때 — 와이어가 바이너리라 curl로 그냥 볼 수 없다.
흥미로운 흐름: Connect(Buf가 만든 프로토콜)가 등장하면서 같은 .proto에서 gRPC와 REST/JSON을 동시에 노출할 수 있다. 내부망은 gRPC로, 외부 클라이언트는 같은 핸들러를 JSON으로 — 한 코드베이스로 두 마리 토끼를 잡는다.
6장 · 같은 모델, 네 가지 포맷 — User-with-orders
추상적으로 비교하지 말고 같은 도메인을 옮겨보자. 사용자가 주문 목록을 갖는다.
6.1 JSON Schema (단독)
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"Order": {
"type": "object",
"required": ["id", "total", "status"],
"properties": {
"id": { "type": "string", "format": "uuid" },
"total": { "type": "number", "minimum": 0 },
"status": { "type": "string", "enum": ["pending","paid","shipped","cancelled"] }
}
}
},
"type": "object",
"required": ["id", "email", "orders"],
"properties": {
"id": { "type": "string", "format": "uuid" },
"email": { "type": "string", "format": "email" },
"orders":{ "type": "array", "items": { "$ref": "#/$defs/Order" } }
}
}
6.2 OpenAPI 3.1 (REST)
paths:
/users/{id}:
get:
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/UserWithOrders' }
components:
schemas:
UserWithOrders:
type: object
required: [id, email, orders]
properties:
id: { type: string, format: uuid }
email: { type: string, format: email }
orders:
type: array
items: { $ref: '#/components/schemas/Order' }
Order:
type: object
required: [id, total, status]
properties:
id: { type: string, format: uuid }
total: { type: number, minimum: 0 }
status: { type: string, enum: [pending, paid, shipped, cancelled] }
6.3 GraphQL SDL
type User {
id: ID!
email: String!
orders: [Order!]!
}
type Order {
id: ID!
total: Float!
status: OrderStatus!
}
enum OrderStatus { PENDING PAID SHIPPED CANCELLED }
type Query {
user(id: ID!): User
}
6.4 Protobuf
message User {
string id = 1;
string email = 2;
repeated Order orders = 3;
}
message Order {
string id = 1;
double total = 2;
OrderStatus status = 3;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_PAID = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_CANCELLED = 4;
}
같은 모델, 네 가지 표현. 정보량은 비슷하지만 누구를 위해 쓰는가가 다르다. JSON Schema는 공통 문법, OpenAPI는 HTTP 노출, GraphQL은 클라이언트 친화, Protobuf는 와이어 효율. 도구를 고르는 일은 데이터 모양이 아니라 대화 상대를 정하는 일이다.
7장 · Smithy — AWS의 API IDL
Smithy는 AWS가 만든 API 정의 언어다. 처음엔 AWS 내부에서 자기 SDK들을 자동 생성하기 위해 만든 도구였지만, 2020년대 후반부터 외부로 풀려 활발히 진화하고 있다.
Smithy의 철학은 "프로토콜에 불가지인 모델". .smithy 파일로 모델을 한 번 정의하면, 거기서 여러 프로토콜로 투영할 수 있다 — REST(JSON over HTTP), AWS 고유 프로토콜, gRPC. OpenAPI는 결과물로 뽑힌다.
$version: "2.0"
namespace shop
service Orders {
version: "2026-05-14"
operations: [GetUser]
}
@http(method: "GET", uri: "/users/{userId}")
operation GetUser {
input := {
@httpLabel
@required
userId: String
}
output := {
@required
id: String
@required
email: String
@required
orders: OrderList
}
}
list OrderList { member: Order }
structure Order {
@required
id: String
@required
total: Double
@required
status: OrderStatus
}
enum OrderStatus {
PENDING
PAID
SHIPPED
CANCELLED
}
Smithy의 강점:
- 트레이트 시스템이 OpenAPI보다 강력하다 — 권한, 페이지네이션, idempotency 같은 횡단 관심사를 깔끔히 표현.
- AWS SDK들(JS·Python·Go·Rust 등)이 모두 Smithy로 생성된다 — 산업적 검증.
- 모델 → 여러 산출물 — OpenAPI도, 클라이언트 SDK도, 서버 스텁도 한 모델에서.
약점: 생태계가 OpenAPI보다 좁다. AWS 외부에서의 채택은 천천히 늘고 있지만 OpenAPI 3.1의 보편성에는 미치지 못한다. "AWS SDK를 만든다"는 시나리오가 아니라면 OpenAPI로 충분한 경우가 많다.
8장 · TypeSpec — Microsoft의 OpenAPI 저작 대안
OpenAPI 3.1 YAML을 손으로 쓰는 건 — 솔직히 — 길고, 반복적이고, 오타가 나기 쉽다. 그 통증의 답으로 Microsoft가 만든 게 TypeSpec(이전 이름 Cadl)이다.
TypeSpec은 TypeScript 비슷한 간결한 DSL로 API를 쓰고, 거기서 OpenAPI(또는 JSON Schema·Protobuf)를 생성한다. 작성성·재사용성·도구 지원이 OpenAPI 원시 YAML보다 한 단계 위다.
import "@typespec/http";
using TypeSpec.Http;
@service({ title: "Orders API" })
namespace ShopApi;
model Order {
id: string;
total: float64;
status: OrderStatus;
}
enum OrderStatus { Pending, Paid, Shipped, Cancelled }
model User {
id: string;
email: string;
orders: Order[];
}
@route("/users/{userId}")
interface Users {
@get op getUser(@path userId: string): User;
}
같은 정보량의 OpenAPI YAML보다 훨씬 짧다. 그리고 컴파일된다 — TypeSpec 컴파일러가 OpenAPI 3.1, JSON Schema 2020-12, Protobuf, 클라이언트 SDK를 동시에 뽑는다. Azure의 새 서비스들은 거의 모두 TypeSpec으로 작성된다.
언제 TypeSpec인가:
- API가 크고 복잡하다 — 수십 개 엔드포인트, 공통 모델·응답 패턴이 많다.
- 한 정의에서 여러 산출물을 뽑고 싶다 — OpenAPI + Protobuf + 클라이언트.
- 팀이 TypeScript에 익숙해 DSL이 친숙하다.
언제 TypeSpec이 아닌가:
- API가 작다 — OpenAPI YAML 한 파일이 더 단순할 수 있다.
- 도구 체인을 단순하게 유지하고 싶다 — TypeSpec은 빌드 단계를 하나 더 추가한다.
9장 · Pact — 계약 테스트, 스키마의 두 번째 사용처
지금까지의 포맷들은 모두 문서 + 코드 생성용이다. 그런데 스키마의 또 다른 강력한 용도가 있다 — 계약 테스트(consumer-driven contract testing). 그 대표가 Pact다.
상황: 서비스 A가 서비스 B를 호출한다. B가 응답 모양을 바꾸면 A가 깨진다. "통합 테스트"는 두 서비스를 동시에 띄워 호출해보지만 — 느리고 깨지기 쉽다. Pact는 다른 접근을 한다.
소비자 주도 계약: A(소비자)가 "나는 B에게 이렇게 요청하고, 이런 모양의 응답을 기대한다"를 계약 파일로 적는다. 이 계약을 Pact Broker에 올린다. B(제공자)는 CI에서 자기 코드가 그 계약을 만족시키는지 검증한다. B가 응답 모양을 바꾸면 — 그리고 A가 그걸 의존한다면 — B의 CI에서 즉시 실패한다.
// 소비자 측 테스트
provider
.uponReceiving('a request for a user')
.withRequest({ method: 'GET', path: '/users/123' })
.willRespondWith({
status: 200,
body: { id: '123', email: 'a@b.com', orders: eachLike({ id: like('o1'), total: like(99.0) }) },
})
Pact는 OpenAPI/AsyncAPI와 경쟁하는 게 아니라 보완한다. OpenAPI는 "API가 이래야 한다"는 정의고, Pact는 "이 소비자가 실제로 이걸 의존한다"는 증거다. 둘을 같이 쓰는 팀이 늘었다.
10장 · AI 도구 호출 — JSON Schema의 두 번째 봄
2024~2025년에 LLM이 도구를 호출하기 시작하면서 JSON Schema에 두 번째 봄이 왔다. OpenAI·Anthropic·Gemini의 도구 정의 포맷은 전부 JSON Schema 서브셋이다. 모델은 그 스키마에 맞춰 arguments를 JSON으로 채워 반환한다.
같은 User-with-orders 도메인에서 "사용자 주문 목록 조회" 도구를 정의해 보자.
{
"name": "get_user_orders",
"description": "Return all orders for the given user",
"input_schema": {
"type": "object",
"required": ["user_id"],
"properties": {
"user_id": { "type": "string", "format": "uuid" },
"status": {
"type": "string",
"enum": ["pending","paid","shipped","cancelled"]
},
"limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
},
"additionalProperties": false
}
}
이게 OpenAPI 안의 components.schemas와 거의 똑같다는 것에 주목하자. 2026년의 깔끔한 워크플로는 이렇다:
- OpenAPI 3.1로 진실의 원본을 둔다.
- 같은 스펙에서 서버 스텁, 클라이언트 SDK를 뽑는다.
- 같은 스펙의
components.schemas를 AI 도구 정의로 매핑한다 — 한 핸들러가 사람과 모델 양쪽에 노출된다. - 같은 스키마로 도구 호출 인자를 검증한다 — 모델이 잘못 채우면 즉시 거부.
"API"와 "AI 에이전트의 도구"가 같은 스키마를 공유한다는 건 2026년의 흔한 현실이다. JSON Schema 2020-12를 단단히 이해하는 게 그래서 백엔드 엔지니어에게 베이스 스킬이 됐다.
11장 · Spec-first vs Code-first — 끝나지 않은 논쟁
OpenAPI/AsyncAPI 시대의 큰 논쟁이 두 가지 진영이다.
Code-first: 코드(데코레이터·타입)를 먼저 쓰고, 거기서 스펙을 생성한다. FastAPI, NestJS, tRPC가 대표. 장점은 단일 진실 — 코드가 스펙이다. 단점은 스펙이 코드의 그림자가 되어, 비기술 이해관계자와의 협의가 늦어지고, 언어 표현력에 갇힌다.
Spec-first: 스펙을 먼저 쓰고, 거기서 서버 스텁과 클라이언트를 생성한다. OpenAPI YAML, TypeSpec, Smithy가 대표. 장점은 프로토콜 명세가 코드보다 먼저 존재한다 — API 디자이너·QA·외부 클라이언트가 동시에 본다. 단점은 두 곳에서 진실이 미끄러질 위험 — 스펙과 핸들러가 어긋날 수 있다.
2026년의 균형점:
- 외부에 노출되는 API, 여러 클라이언트(언어·팀)가 있는 API → spec-first가 거의 항상 유리.
- 내부 단일 서비스, 빠른 프로토타이핑, 풀스택 한 팀 → code-first가 빠르다.
- 하이브리드 가능: code-first로 시작해 스펙이 안정되면 spec-first로 고착하는 흐름.
도구 차이도 좁아졌다. TypeSpec·Smithy 같은 고수준 spec-first 도구는 작성 마찰을 크게 줄였고, FastAPI 같은 대표적 code-first 도구는 거꾸로 스펙 정합성 검증을 강화했다. 진영 논쟁보다 "이 API의 수명과 클라이언트 수"를 보는 게 정직하다.
12장 · 도구 체인 — 2026년 표준 스택
| 영역 | 표준에 가까운 도구 |
|---|---|
| OpenAPI 문서 UI | Scalar, Stoplight Elements (Swagger UI는 레거시) |
| OpenAPI 편집기 | Stoplight Studio, Insomnia, Postman |
| 코드 생성(클라이언트) | openapi-typescript, oapi-codegen, openapi-generator |
| Mock 서버 | Prism (Stoplight), Mockoon |
| 계약 테스트 | Pact (Pact Broker, PactFlow) |
| Protobuf 도구체인 | Buf (lint, breaking-change, BSR), protoc (저수준) |
| Protobuf → REST | Connect (Buf), gRPC-Gateway |
| AsyncAPI 편집기 | AsyncAPI Studio |
| AsyncAPI 코드젠 | AsyncAPI Generator |
| GraphQL 도구 | Apollo Studio, GraphiQL, GraphQL Code Generator, Hasura, Relay |
| TypeSpec | tsp CLI, VS Code 확장 |
| Smithy | smithy build, IntelliJ 플러그인 |
Swagger UI를 기본으로 두는 시대는 끝났다. Stoplight Elements와 Scalar 둘 다 OpenAPI 3.1을 깔끔히 다루고, 다크모드·검색·코드 샘플·테마가 훨씬 낫다. 새 프로젝트에서 Scalar의 임베드 스니펫 한 줄로 문서 페이지가 끝난다.
13장 · 의사결정 프레임 — 우리 팀은 무엇을 써야 하나
같은 "API"라도 대화 상대가 다르면 답이 달라진다.
질문 1: 대화 상대는 누구인가?
├─ 외부 개발자(공개 API) → OpenAPI 3.1 + Scalar/Stoplight
├─ 사내 프런트엔드(여러 모양 필요) → GraphQL (Apollo Federation)
├─ 내부 서비스 ↔ 서비스 → gRPC + Protobuf (Buf)
├─ 이벤트(카프카·MQTT·WebSocket) → AsyncAPI 3.0
└─ AI 에이전트의 도구 → JSON Schema 2020-12 (OpenAPI에서 재사용)
질문 2: API의 수명은?
├─ 단기 프로토타입 → code-first (FastAPI 등)로 빠르게
└─ 장기·외부 노출 → spec-first (OpenAPI/TypeSpec/Smithy)로 단단히
질문 3: 한 정의에서 여러 산출물이 필요한가?
├─ 예 (REST + gRPC + 클라이언트 SDK) → TypeSpec 또는 Smithy
└─ 아니오 → OpenAPI 단독으로 충분
질문 4: 클라이언트가 깨지는 걸 얼마나 두려워하는가?
├─ 매우 두렵다 → Pact로 계약 테스트 추가
└─ 적당히 → 스펙 + 코드젠으로 충분
추가로, 2026년에 권장하지 않는 조합:
- 새 프로젝트를 **OpenAPI 2.0(Swagger 2)**으로 시작하는 것 — 10년 묵은 규격이다. 무조건 3.1.
- gRPC를 브라우저에 직접 노출 — gRPC-Web/Connect를 거치거나 GraphQL/REST를 별도 BFF로.
protoc를 직접 호출 — Buf가 거의 모든 면에서 우위.- Swagger UI를 새 표준으로 두는 것 — 2026년에는 Scalar 또는 Stoplight Elements.
- GraphQL의 임의 쿼리를 외부에 그대로 — persisted queries로 화이트리스트.
14장 · 마이그레이션 — 흔한 경로 세 가지
A. OpenAPI 3.0 → 3.1
가장 흔한 마이그레이션이다. 호환성이 매우 높아 대부분 기계적으로 끝난다:
nullable: true→"type": ["string", "null"](또는 anyOf로null).example→examples(스칼라 1개라면 그대로 둬도 됨).- 일부 도구의 메타 어노테이션(
x-확장) 점검. - CI에 OpenAPI 3.1 검증기 추가.
B. 단일 OpenAPI → OpenAPI + AsyncAPI 분리
REST와 이벤트가 한 파일에 어지럽게 섞여 있던 코드베이스에서, AsyncAPI로 이벤트를 분리한다:
- 카프카 토픽·메시지를 추출해 AsyncAPI 3.0 파일로 옮긴다.
- 메시지 페이로드 스키마는 공유 JSON Schema 파일로 떼어내 둘 다
$ref로 가리킨다. - 한 저장소에
openapi.yaml,asyncapi.yaml,schemas/가 나란히 산다.
C. 손으로 쓰는 OpenAPI → TypeSpec/Smithy로 끌어올리기
API가 커지고 반복이 심해질 때:
- 기존 OpenAPI를 TypeSpec/Smithy로 임포트한다(공식 변환기 존재).
- 공통 모델·트레이트(권한·페이지네이션·idempotency)를 횡단으로 추출.
- TypeSpec/Smithy를 진실의 원본으로, OpenAPI는 생성 산출물로 격하.
세 경로 모두 큰 빅뱅이 아니라 점진적이다. 2026년의 흔한 실수는 "한 번에 다 갈아엎자"인데, 거의 항상 실패한다. 한 영역(예: 한 서비스, 한 토픽)을 옮긴 뒤 패턴을 굳히고 확장한다.
15장 · 신뢰성 — 스펙이 실제로 지켜지게 만들기
스키마가 진실이 되려면 지켜져야 한다. 2026년의 흔한 가드레일:
- CI에서 OpenAPI/AsyncAPI 린트 — Spectral, Redocly가 표준. 스타일·금지 패턴·필수 필드 검사.
- Breaking-change 검출 — Buf의
buf breaking, OpenAPI는oasdiff. PR이 스펙을 깨면 즉시 빨간불. - 서버 응답을 실제로 스펙과 대조 — 라이브 트래픽 일부를 스펙 검증기에 흘려본다(예: Optic, prism --validate).
- 계약 테스트(Pact) — 소비자가 실제로 의존하는 모양만 잡아낸다.
- AI 도구 호출의 인자 검증 — 모델이 잘못 채우면 즉시 거부하고 모델에게 정정 요청.
이 다섯이 다 있을 필요는 없지만 하나는 있어야 한다. "스펙이 있다"와 "스펙이 지켜진다"는 다른 일이다.
에필로그 — 스키마가 진실이다
2026년 백엔드의 한 줄 정리: API의 단위는 엔드포인트가 아니라 스키마다.
올바른 스키마 한 파일에서 — 문서·클라이언트·서버 스텁·테스트 픽스처·AI 도구 정의가 전부 나온다. 좋은 스키마는 외부 개발자에게 "지금 무엇이 있는가"를 보여주고, 내부 팀에게 "무엇이 깨지면 안 되는가"를 알려주고, AI 에이전트에게 "어떻게 부르면 되는가"를 가르친다.
도구는 많다 — JSON Schema, OpenAPI 3.1, AsyncAPI 3.0, GraphQL, Protobuf, Smithy, TypeSpec, Pact. 외워야 할 이름이 늘어난 게 아니라, 역할이 분화된 것이다. 한 도구가 모든 상황을 이기는 일은 없다. 좋은 백엔드 엔지니어는 대화 상대에 따라 도구를 고른다.
14개 항목 체크리스트
- 새 프로젝트의 OpenAPI는 3.1이다(2.0/3.0이 아니라).
- 같은 저장소에 REST(OpenAPI)와 이벤트(AsyncAPI)가 나란히 있다.
- 메시지 페이로드 스키마는 공유 JSON Schema로 떼어 양쪽이
$ref한다. - Swagger UI 대신 Scalar 또는 Stoplight Elements를 쓴다.
- CI에 OpenAPI/AsyncAPI 린트(Spectral 등)가 있다.
- Breaking-change 감지가 CI에 있다(
buf breaking,oasdiff). - 클라이언트 SDK가 스펙에서 자동 생성된다.
- 서버 스텁/타입도 스펙에서 생성된다(손코딩 금지).
- Protobuf 도구체인은 Buf다 —
protoc를 직접 쓰지 않는다. - gRPC를 브라우저에 직접 노출하지 않는다(Connect/gRPC-Web/BFF).
- AI 도구 정의가 같은 JSON Schema 컴포넌트를 재사용한다.
- AI 도구 호출의 인자를 실행 전에 스키마로 검증한다.
- 핵심 통합에는 Pact(또는 동등한 계약 테스트)가 있다.
- GraphQL은 임의 쿼리가 아니라 persisted queries로 화이트리스트된다.
안티패턴 10가지
- 코드와 스펙을 둘 다 손으로 유지하려는 시도 — 반드시 미끄러진다.
- 모든 곳에 GraphQL을 — CRUD에는 OpenAPI가 더 단순하다.
- 모든 곳에 gRPC를 — 브라우저·외부 개발자에겐 마찰이 크다.
- OpenAPI는 있지만 검증되지 않는 상태 — 그건 위키 문서다, 스펙이 아니다.
- 이벤트를 OpenAPI에 억지로 박는 것 — AsyncAPI 3.0이 답이다.
- Swagger 2를 오늘 시작하는 것 — 10년 전 규격이다.
protoc를 손으로 호출 — Buf가 모든 면에서 우위.- Smithy를 AWS SDK 만들 때가 아닌데 강제로 — 생태계 가성비가 낮다.
- AI 도구 정의를 OpenAPI와 별도로 유지 — 같은 컴포넌트 재사용이 정답.
- 한 번에 모든 API를 spec-first로 갈아엎는 빅뱅 — 점진적이지 않으면 실패.
다음 글 예고
다음 글 후보: Connect 프로토콜 심층 — Buf가 만든 gRPC + REST의 통합, AsyncAPI 3.0으로 사내 이벤트 카탈로그 만들기, AI 도구 호출 스키마 설계 — JSON Schema를 모델 친화적으로 쓰는 법.
"엔드포인트가 아니라 스키마다. 2026년의 좋은 API는 손으로 코딩되는 게 아니라, 스키마에서 떨어진다."
— 2026 API 스키마 지도, 끝.
참고 / References
- JSON Schema — draft 2020-12 specification
- JSON Schema — Getting Started
- OpenAPI Specification 3.1.0
- OpenAPI Initiative
- OpenAPI 3.2 work-in-progress
- AsyncAPI 3.0 specification
- AsyncAPI Initiative
- AsyncAPI 3.0 release notes
- GraphQL spec — June 2018 (current)
- GraphQL Foundation
- Apollo Federation v2 docs
- Protocol Buffers — google/protobuf
- gRPC project home
- Buf — Protobuf toolchain
- Buf Schema Registry (BSR)
- Connect protocol — Buf
- Smithy — AWS API IDL
- Smithy GitHub — smithy-lang/smithy
- TypeSpec — Microsoft
- TypeSpec GitHub — microsoft/typespec
- Pact docs
- PactFlow
- Stoplight Elements
- Scalar — API docs
- Redocly
- Spectral — OpenAPI/AsyncAPI linter
- oasdiff — OpenAPI diff
- OpenAI — function calling
- Anthropic — tool use
- Gemini — function calling
- openapi-typescript
- openapi-generator
API Schema Landscape 2026 — JSON Schema, OpenAPI 3.1, AsyncAPI, GraphQL, gRPC, Smithy, TypeSpec in One Map
Prologue — schemas now come before code
A 2026 backend meeting usually opens with one sentence: "Don't sketch the endpoint yet — show me the schema."
There was an era where you wrote the handler first and then auto-generated an OpenAPI document out of it. Spec-from-code — the code was truth, the spec was a byproduct. It felt easy. It also broke. Two teams guessed at each other's response shapes and produced month-long integration bugs, client SDKs were always one step behind, and the internal "current API" wiki page was half lies.
2026 inverted that. Spec-first — write the schema first, then generate types, docs, server stubs, client SDKs, test fixtures and AI tool definitions out of it. The truth lives in a schema file.
So this article maps it out. "API" looks like one word but the right tool changes with the protocol. REST uses OpenAPI 3.1. Events use AsyncAPI 3.0. Service-to-service uses Protobuf. Frontend-friendly graphs use GraphQL. The grammar underneath all of them is JSON Schema 2020-12. Then add AWS's Smithy, Microsoft's TypeSpec, the contract-testing tool Pact — many names, but the roles are crisp.
This piece is the 2026 schema atlas. The identity and strengths of each format, the same domain model rendered in four, the new role JSON Schema took on inside AI tool calling, and an honest decision frame for "what should my team actually use."
1. JSON Schema 2020-12 — the foundation of everything
One line first: JSON Schema is a standard for describing the shape of a JSON value in more JSON. "This field is an integer, this one is between 1 and 120, this one is an ISO 8601 date" — those constraints written as declarations.
Draft 2020-12 is the de-facto stable baseline. Work after it continues, but as of 2026 the version with the deepest compatibility and tooling is 2020-12. OpenAPI 3.1 aligned fully with this draft, and the tiny historic frictions (the old nullable/example differences that used to bite us) are gone.
Why JSON Schema matters in 2026: it shows up in three different jobs at once.
- REST APIs: the inside of OpenAPI 3.1's
components.schemasis literally JSON Schema 2020-12. - AI tool calling (function calling): the tool definition formats of OpenAI, Anthropic and Gemini are all JSON Schema subsets. The model returns
argumentsfilled in as JSON, and the host validates them against the same schema. - Config validation: the standard you most often see when validating a kubeconfig, a CI workflow, or an app config file.
JSON Schema is not a format, it is a grammar. Learn it once and you use it in all three places. It is arguably the single standard a backend engineer touches most often in 2026.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/user.json",
"type": "object",
"required": ["id", "email"],
"properties": {
"id": { "type": "string", "format": "uuid" },
"email": { "type": "string", "format": "email" },
"age": { "type": "integer", "minimum": 0, "maximum": 130 },
"roles": {
"type": "array",
"items": { "type": "string", "enum": ["admin", "member", "guest"] },
"uniqueItems": true
}
},
"additionalProperties": false
}
This small file slots into OpenAPI, into an AI tool definition, into a standalone validator. The reusable unit is the truth.
2. OpenAPI 3.1 — the REST standard is settled, and only just starting
The headline difference between OpenAPI 3.0 and 3.1 is one line. 3.1 aligned fully with JSON Schema 2020-12. The 3.0-era nullable: true is gone — the standard is "type": ["string", "null"]. The semantics of example/examples are cleaned up. $ref got freer — it can point at any JSON Schema.
In 2026 OpenAPI 3.1 is the de-facto standard for REST API specs. The Swagger-era residue mostly washed out, and new projects almost universally start at 3.1. OpenAPI 3.2 is in motion (tidying smaller surfaces around forms, headers, multi-content responses), but 3.1 and 3.2 evolve without breaking each other. Starting at 3.1 today is safe.
openapi: 3.1.0
info:
title: Orders API
version: 1.0.0
paths:
/users/{userId}/orders:
get:
summary: List orders for a user
parameters:
- name: userId
in: path
required: true
schema: { type: string, format: uuid }
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Order'
components:
schemas:
Order:
type: object
required: [id, userId, total]
properties:
id: { type: string, format: uuid }
userId:{ type: string, format: uuid }
total: { type: number, minimum: 0 }
status:
type: string
enum: [pending, paid, shipped, cancelled]
What gets generated out of this single file: TypeScript/Go/Python client SDKs, server stubs, Postman/Bruno collections, interactive docs, mock servers, contract-test fixtures, AI tool definitions an agent can call. That is the 2026 workflow — one spec, every artifact.
Swagger UI still runs, but the new defaults are Stoplight Elements and Scalar. Both treat OpenAPI 3.1 as native, both render dark mode, search, "try it" and code samples far more cleanly. Scalar in particular spread fast because of its one-script-tag embed. Swagger UI is now an optional choice, not the default.
3. AsyncAPI 3.0 — the OpenAPI of events
REST has OpenAPI. So what about Kafka topics, MQTT channels, WebSocket streams, Server-Sent Events? The 2026 answer is crisp — AsyncAPI 3.0.
The loudest critique of AsyncAPI 2.x was the ambiguity of "channel equals topic equals operation bundle." Publish and subscribe were tangled inside the same channel definition. 3.0 separated those into explicit operations. A channel is the path the messages flow over, an operation is the act of sending or receiving. That single split alone made systems like Kafka much cleaner to model.
The core vocabulary of AsyncAPI 3.0:
- channels — the paths messages flow over (topic, queue, channel).
- operations — what happens on a channel (send, receive). Explicit.
- messages — the payload schemas (reusing JSON Schema directly).
- servers — Kafka brokers, MQTT brokers, WebSocket endpoints — protocol-specific bindings.
asyncapi: 3.0.0
info:
title: Orders Events
version: 1.0.0
servers:
kafka-prod:
host: kafka.prod.example.com:9092
protocol: kafka
channels:
orderCreated:
address: orders.created.v1
messages:
OrderCreated:
$ref: '#/components/messages/OrderCreated'
operations:
publishOrderCreated:
action: send
channel: { $ref: '#/channels/orderCreated' }
components:
messages:
OrderCreated:
payload:
type: object
required: [orderId, userId, createdAt]
properties:
orderId: { type: string, format: uuid }
userId: { type: string, format: uuid }
createdAt: { type: string, format: date-time }
total: { type: number, minimum: 0 }
Two reasons AsyncAPI 3.0 settled in 2026. First, event-driven systems are not a special case anymore — almost every microservices shop runs Kafka, NATS or MQTT. Second, the AsyncAPI ecosystem stopped imitating OpenAPI and grew its own tools — Studio, generator (server stubs, clients), validator, catalog. The usability gap mostly closed.
When REST and events coexist in the same system — which is normal in 2026 — the standard pattern is to keep OpenAPI and AsyncAPI side by side in one repo. Both stand on JSON Schema, so sharing message payload schemas across them is easy.
4. GraphQL SDL — the peace treaty with the frontend
GraphQL was published by Facebook in 2015, moved to the GraphQL Foundation in 2019, and as of 2026 still holds a strong niche. Especially where many clients need different shapes of data — BFFs, content platforms with several mobile apps, dashboards.
GraphQL schemas are written in their own DSL — SDL (Schema Definition Language). Unlike OpenAPI, AsyncAPI and JSON Schema, it is not JSON.
type User {
id: ID!
email: String!
age: Int
orders(status: OrderStatus, limit: Int = 20): [Order!]!
}
type Order {
id: ID!
total: Float!
status: OrderStatus!
createdAt: DateTime!
}
enum OrderStatus {
PENDING
PAID
SHIPPED
CANCELLED
}
type Query {
user(id: ID!): User
orders(userId: ID!, status: OrderStatus): [Order!]!
}
scalar DateTime
GraphQL's charm is introspection — the schema itself is queryable from the API. Client tools point at a live server, pull the schema, and codegen typed hooks. Apollo Studio, GraphiQL, GraphQL Code Generator, urql, Relay — the toolbox is thick.
2026 GraphQL trends:
- Federation v2 is settled — the standard for composing many services' GraphQLs into a single supergraph.
- Persisted queries became the security and performance default — clients ship a hash of a build-time-registered query, not arbitrary text.
- Coexistence with REST. The "rewrite everything in GraphQL" wave is over. A common pattern: GraphQL where the frontend churns, gRPC/REST between backends.
Be honest about the weaknesses. Caching is awkward — one URL means HTTP caches do not help much. N+1 must be solved with patterns like DataLoader. Without query cost analysis, a single client can knock the server over. And it does not fit every place — for a simple CRUD API, OpenAPI is almost always simpler.
5. gRPC plus Protobuf — still the default between services
Service-to-service in 2026? gRPC plus Protobuf is still the default. The reasons are simple — small, fast, polyglot, streaming is first-class.
Protobuf is a binary schema. Define messages and services in a .proto file, and protoc (or more often Buf) generates per-language code. On the wire it is far smaller than JSON (field names compress into integer tags) and far faster to parse.
syntax = "proto3";
package shop.v1;
import "google/protobuf/timestamp.proto";
message User {
string id = 1;
string email = 2;
int32 age = 3;
repeated string roles = 4;
}
message Order {
string id = 1;
string user_id = 2;
double total = 3;
OrderStatus status = 4;
google.protobuf.Timestamp created_at = 5;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_PAID = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_CANCELLED = 4;
}
service OrdersService {
rpc GetOrders(GetOrdersRequest) returns (GetOrdersResponse);
rpc StreamOrders(stream GetOrdersRequest) returns (stream Order);
}
message GetOrdersRequest { string user_id = 1; }
message GetOrdersResponse { repeated Order orders = 1; }
The 2026 center of gravity for the Protobuf ecosystem is Buf. buf build, buf lint, buf breaking, buf generate — the entire usability gap of raw protoc is closed. Buf Schema Registry plays the role of "GitHub for protobuf schemas" — versions, releases, dependencies.
When you want gRPC:
- Service-to-service — the internal mesh.
- Bidirectional streaming — chat, live data, game servers.
- High performance where it really matters — very high RPS, big payloads.
When you do not:
- Direct from the browser — gRPC-Web exists, but it is awkward. REST or GraphQL is friendlier.
- Public third-party APIs — too high a bar for outside developers.
- Debug by curl — the wire is binary, you cannot just
curl | jqit.
A notable trend: Connect (the protocol Buf authored) lets the same .proto expose both gRPC and REST/JSON. Internal mesh in gRPC, external clients hitting the same handlers in JSON — one codebase, two birds.
6. Same model, four formats — User-with-orders
Stop comparing abstractly and move the same domain across formats. A user owns an array of orders.
6.1 JSON Schema (standalone)
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"Order": {
"type": "object",
"required": ["id", "total", "status"],
"properties": {
"id": { "type": "string", "format": "uuid" },
"total": { "type": "number", "minimum": 0 },
"status": { "type": "string", "enum": ["pending","paid","shipped","cancelled"] }
}
}
},
"type": "object",
"required": ["id", "email", "orders"],
"properties": {
"id": { "type": "string", "format": "uuid" },
"email": { "type": "string", "format": "email" },
"orders":{ "type": "array", "items": { "$ref": "#/$defs/Order" } }
}
}
6.2 OpenAPI 3.1 (REST)
paths:
/users/{id}:
get:
parameters:
- { name: id, in: path, required: true, schema: { type: string, format: uuid } }
responses:
'200':
content:
application/json:
schema: { $ref: '#/components/schemas/UserWithOrders' }
components:
schemas:
UserWithOrders:
type: object
required: [id, email, orders]
properties:
id: { type: string, format: uuid }
email: { type: string, format: email }
orders:
type: array
items: { $ref: '#/components/schemas/Order' }
Order:
type: object
required: [id, total, status]
properties:
id: { type: string, format: uuid }
total: { type: number, minimum: 0 }
status: { type: string, enum: [pending, paid, shipped, cancelled] }
6.3 GraphQL SDL
type User {
id: ID!
email: String!
orders: [Order!]!
}
type Order {
id: ID!
total: Float!
status: OrderStatus!
}
enum OrderStatus { PENDING PAID SHIPPED CANCELLED }
type Query {
user(id: ID!): User
}
6.4 Protobuf
message User {
string id = 1;
string email = 2;
repeated Order orders = 3;
}
message Order {
string id = 1;
double total = 2;
OrderStatus status = 3;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_PAID = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_CANCELLED = 4;
}
Same model, four expressions. The information content is roughly equal, but the audience differs. JSON Schema is the shared grammar, OpenAPI is the HTTP surface, GraphQL is client-friendly, Protobuf is wire-efficient. Picking a tool is not about the shape of the data, it is about who you are talking to.
7. Smithy — AWS's API IDL
Smithy is the API definition language built by AWS. It started as an internal tool for generating AWS's own SDKs, but since the late 2020s it has been published outward and is actively evolving.
Smithy's philosophy is "protocol-agnostic model." You define a model once in .smithy files, then project it onto several protocols — REST (JSON over HTTP), AWS-specific protocols, gRPC. OpenAPI is an output.
$version: "2.0"
namespace shop
service Orders {
version: "2026-05-14"
operations: [GetUser]
}
@http(method: "GET", uri: "/users/{userId}")
operation GetUser {
input := {
@httpLabel
@required
userId: String
}
output := {
@required
id: String
@required
email: String
@required
orders: OrderList
}
}
list OrderList { member: Order }
structure Order {
@required
id: String
@required
total: Double
@required
status: OrderStatus
}
enum OrderStatus {
PENDING
PAID
SHIPPED
CANCELLED
}
Strengths of Smithy:
- A trait system more expressive than OpenAPI — auth, pagination, idempotency and other cross-cutting concerns expressed cleanly.
- All AWS SDKs (JavaScript, Python, Go, Rust and more) are generated from Smithy — industrial validation.
- One model, many outputs — OpenAPI, client SDKs, server stubs all out of one source.
Weakness: the ecosystem is narrower than OpenAPI's. Adoption outside AWS is growing, but it does not reach OpenAPI 3.1's universality. Unless your story really is "we are publishing SDKs across many languages," OpenAPI is often enough.
8. TypeSpec — Microsoft's authoring layer over OpenAPI
Writing OpenAPI 3.1 YAML by hand is — honestly — long, repetitive and easy to typo. Microsoft's answer to that pain is TypeSpec (formerly Cadl).
TypeSpec is a compact DSL (TypeScript-flavored) for authoring APIs, from which you generate OpenAPI (or JSON Schema, or Protobuf). It is a level above raw OpenAPI YAML on authorability, reuse and tooling.
import "@typespec/http";
using TypeSpec.Http;
@service({ title: "Orders API" })
namespace ShopApi;
model Order {
id: string;
total: float64;
status: OrderStatus;
}
enum OrderStatus { Pending, Paid, Shipped, Cancelled }
model User {
id: string;
email: string;
orders: Order[];
}
@route("/users/{userId}")
interface Users {
@get op getUser(@path userId: string): User;
}
Far shorter than the equivalent OpenAPI YAML. And it compiles — the TypeSpec compiler emits OpenAPI 3.1, JSON Schema 2020-12, Protobuf and client SDKs in one shot. Most new Azure services are authored in TypeSpec.
Use TypeSpec when:
- The API is big and complex — dozens of endpoints, many shared models and response patterns.
- You need multiple outputs from one definition — OpenAPI plus Protobuf plus clients.
- The team is fluent in TypeScript and the DSL feels native.
Avoid TypeSpec when:
- The API is small — a single OpenAPI YAML may simply be simpler.
- You want the toolchain to stay tiny — TypeSpec is another build step.
9. Pact — contract testing, the second use of schemas
Everything so far is documentation plus code generation. There is another strong use of schemas — consumer-driven contract testing. The flagship is Pact.
Setup: service A calls service B. If B reshapes its responses, A breaks. "Integration tests" mean spinning both up — slow and flaky. Pact takes a different route.
Consumer-driven contracts. A (the consumer) writes a contract file that says "I send B this request, I expect this shape back." The contract is uploaded to a Pact Broker. B (the provider) verifies in CI that its real code satisfies that contract. If B reshapes its response — and A depends on it — B's CI goes red immediately.
// consumer-side test
provider
.uponReceiving('a request for a user')
.withRequest({ method: 'GET', path: '/users/123' })
.willRespondWith({
status: 200,
body: { id: '123', email: 'a@b.com', orders: eachLike({ id: like('o1'), total: like(99.0) }) },
})
Pact does not compete with OpenAPI/AsyncAPI — it complements them. OpenAPI is the definition of what the API should be. Pact is the evidence of what a consumer actually depends on. More and more teams ship both.
10. AI tool calling — a second spring for JSON Schema
In 2024 and 2025, as LLMs started calling tools, JSON Schema had a second spring. The tool definition formats of OpenAI, Anthropic and Gemini are all JSON Schema subsets. The model returns arguments filled in as JSON, and the host validates.
Same User-with-orders domain, expressed as a tool that "fetches a user's orders":
{
"name": "get_user_orders",
"description": "Return all orders for the given user",
"input_schema": {
"type": "object",
"required": ["user_id"],
"properties": {
"user_id": { "type": "string", "format": "uuid" },
"status": {
"type": "string",
"enum": ["pending","paid","shipped","cancelled"]
},
"limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
},
"additionalProperties": false
}
}
Notice this is almost identical to a components.schemas entry inside OpenAPI. The clean 2026 workflow looks like this:
- Keep an OpenAPI 3.1 file as the source of truth.
- Generate server stubs and client SDKs from the same spec.
- Map the spec's
components.schemasto AI tool definitions — one handler exposed to both humans and models. - Validate the model's tool-call arguments against the same schema — wrong shape, immediate rejection.
"API" and "AI agent tool" sharing the same schema is the common reality of 2026. That is exactly why a solid grip on JSON Schema 2020-12 became a base skill for backend engineers.
11. Spec-first vs code-first — the debate that did not end
The OpenAPI/AsyncAPI era split into two camps.
Code-first. Write code (decorators, types) first, generate the spec from it. FastAPI, NestJS, tRPC are the flagships. Upside: a single source of truth — the code is the spec. Downside: the spec becomes a shadow of the code, slower for non-technical stakeholder review, and constrained by what the language expresses well.
Spec-first. Write the spec first, generate server stubs and clients out of it. OpenAPI YAML, TypeSpec, Smithy are the flagships. Upside: the protocol specification exists before the code — designers, QA and external clients see it together. Downside: truth can slip between two places — spec and handler drift.
The 2026 balance:
- External-facing APIs, APIs with many clients (languages, teams) → spec-first is almost always better.
- Internal single service, rapid prototyping, one fullstack team → code-first is faster.
- Hybrid is fine: start code-first, then freeze into spec-first once the spec stabilizes.
Tooling has narrowed the gap. High-level spec-first tools like TypeSpec and Smithy cut the authoring friction; flagship code-first tools like FastAPI tightened spec-conformance checks. Camp warfare is less interesting than asking "how long-lived is this API and how many clients will it have."
12. Toolchain — the 2026 standard stack
| Area | Near-standard tooling |
|---|---|
| OpenAPI doc UI | Scalar, Stoplight Elements (Swagger UI is legacy) |
| OpenAPI editor | Stoplight Studio, Insomnia, Postman |
| Client codegen | openapi-typescript, oapi-codegen, openapi-generator |
| Mock servers | Prism (Stoplight), Mockoon |
| Contract testing | Pact (Pact Broker, PactFlow) |
| Protobuf toolchain | Buf (lint, breaking-change, BSR), protoc (low level) |
| Protobuf to REST | Connect (Buf), gRPC-Gateway |
| AsyncAPI editor | AsyncAPI Studio |
| AsyncAPI codegen | AsyncAPI Generator |
| GraphQL stack | Apollo Studio, GraphiQL, GraphQL Code Generator, Hasura, Relay |
| TypeSpec | tsp CLI, VS Code extension |
| Smithy | smithy build, IntelliJ plugin |
The era of Swagger UI as the default is over. Stoplight Elements and Scalar both handle OpenAPI 3.1 cleanly, with much better dark mode, search, code samples and theming. On a new project, a single Scalar embed snippet is usually the entire docs page.
13. Decision frame — what should your team use
Same word "API," different audience, different answer.
Q1: Who is the audience?
├─ External developers (public API) → OpenAPI 3.1 + Scalar/Stoplight
├─ Internal frontend (many shapes needed) → GraphQL (Apollo Federation)
├─ Internal service-to-service → gRPC + Protobuf (Buf)
├─ Events (Kafka, MQTT, WebSocket) → AsyncAPI 3.0
└─ AI agent's tools → JSON Schema 2020-12 (reuse from OpenAPI)
Q2: How long-lived is the API?
├─ Short prototype → code-first (FastAPI etc.) for speed
└─ Long-lived, external → spec-first (OpenAPI/TypeSpec/Smithy) for solidity
Q3: Do you need multiple outputs from one definition?
├─ Yes (REST + gRPC + client SDKs) → TypeSpec or Smithy
└─ No → plain OpenAPI is enough
Q4: How afraid are you of breaking clients?
├─ Very → add Pact contract tests
└─ Modestly → spec plus codegen is enough
A few combinations to avoid in 2026:
- Starting a new project on OpenAPI 2.0 (Swagger 2) — it is a decade-old spec. Use 3.1.
- Exposing gRPC directly to the browser — go through gRPC-Web/Connect or a BFF.
- Calling
protocdirectly — Buf wins on almost every axis. - Treating Swagger UI as the new default — in 2026 it is Scalar or Stoplight Elements.
- Allowing arbitrary GraphQL queries from external clients — whitelist via persisted queries.
14. Migration — three common paths
A. OpenAPI 3.0 → 3.1
The most common migration. Compatibility is high, so most of it is mechanical:
nullable: true→"type": ["string", "null"](oranyOfwith null).example→examples(a single scalar can sometimes stay).- Audit tool-specific vendor extensions (
x-keys). - Add an OpenAPI 3.1 validator in CI.
B. Single OpenAPI → OpenAPI plus AsyncAPI split
In a repo where REST and events were tangled in one file, split events into AsyncAPI:
- Extract Kafka topics and messages into an AsyncAPI 3.0 file.
- Pull message payload schemas into shared JSON Schema files that both sides
$ref. - Keep
openapi.yaml,asyncapi.yamlandschemas/side by side in one repo.
C. Hand-written OpenAPI → TypeSpec/Smithy
When the API grows large and repetitive:
- Import the existing OpenAPI into TypeSpec/Smithy (official converters exist).
- Lift cross-cutting models and traits (auth, pagination, idempotency).
- Promote TypeSpec/Smithy to the source of truth, demote OpenAPI to a generated artifact.
All three are incremental, not big-bang. A common 2026 mistake is "let's redo all APIs at once" — it almost always fails. Move one area (one service, one topic), solidify the pattern, then expand.
15. Trust — making the spec actually hold
A schema is only truth if it is enforced. Common 2026 guardrails:
- CI lint for OpenAPI/AsyncAPI — Spectral and Redocly are standard. Style rules, banned patterns, required fields.
- Breaking-change detection — Buf's
buf breakingfor protobuf,oasdifffor OpenAPI. A spec-breaking PR turns red. - Validate real responses against the spec — pipe a slice of live traffic into a validator (Optic, prism --validate).
- Contract testing (Pact) — captures only the shapes a consumer actually depends on.
- Argument validation in AI tool calls — reject malformed model output and ask the model to retry.
You do not need all five, but you need one. "Having a spec" and "the spec being honored" are not the same job.
Epilogue — the schema is the truth
A one-line summary of 2026 backend engineering: the unit of an API is the schema, not the endpoint.
Out of one good schema file flow all of the following: docs, clients, server stubs, test fixtures, AI tool definitions. A good schema tells external developers "what exists right now," internal teams "what cannot break," and AI agents "how to call you."
There are many tools — JSON Schema, OpenAPI 3.1, AsyncAPI 3.0, GraphQL, Protobuf, Smithy, TypeSpec, Pact. It is not that you have more names to memorize; it is that the roles split apart. No single tool wins every situation. A good backend engineer picks the tool by the audience.
14-item checklist
- New projects start on OpenAPI 3.1 (not 2.0, not 3.0).
- REST (OpenAPI) and events (AsyncAPI) live side by side in the same repo.
- Message payload schemas live as shared JSON Schema files that both sides
$ref. - Docs are Scalar or Stoplight Elements, not Swagger UI.
- Spec lint (Spectral and friends) runs in CI for both OpenAPI and AsyncAPI.
- Breaking-change detection runs in CI (
buf breaking,oasdiff). - Client SDKs are generated from the spec.
- Server stubs and types are generated from the spec (no hand-coding).
- The Protobuf toolchain is Buf —
protocis not called by hand. - gRPC is not exposed directly to the browser (Connect/gRPC-Web/BFF instead).
- AI tool definitions reuse the same JSON Schema components as the HTTP API.
- AI tool-call arguments are validated against the schema before execution.
- Critical integrations have Pact (or equivalent contract testing).
- GraphQL exposes persisted queries, not arbitrary user queries.
Ten anti-patterns
- Maintaining code and spec both by hand — they always drift.
- GraphQL everywhere — for plain CRUD, OpenAPI is simpler.
- gRPC everywhere — friction is high for browsers and outside developers.
- Having an OpenAPI that is never validated — that is a wiki page, not a spec.
- Forcing events into OpenAPI — AsyncAPI 3.0 is the right answer.
- Starting fresh on Swagger 2 — it is a decade-old spec.
- Calling
protocdirectly — Buf wins everywhere. - Forcing Smithy when you are not shipping multi-language SDKs — ecosystem cost is too high.
- Keeping AI tool definitions separate from the OpenAPI components — reuse the same shapes.
- Big-bang migration of every API to spec-first at once — incremental or fail.
Next-post candidates
Candidates: Connect protocol deep dive — Buf's fusion of gRPC and REST, Building an internal event catalog with AsyncAPI 3.0, Designing AI tool-call schemas — making JSON Schema model-friendly.
"Not the endpoint, the schema. A good 2026 API is not hand-coded — it falls out of the schema."
— API schema atlas 2026, end.
참고 / References
- JSON Schema — draft 2020-12 specification
- JSON Schema — Getting Started
- OpenAPI Specification 3.1.0
- OpenAPI Initiative
- OpenAPI 3.2 work-in-progress
- AsyncAPI 3.0 specification
- AsyncAPI Initiative
- AsyncAPI 3.0 release notes
- GraphQL spec — October 2021
- GraphQL Foundation
- Apollo Federation v2 docs
- Protocol Buffers — google/protobuf
- gRPC project home
- Buf — Protobuf toolchain
- Buf Schema Registry (BSR)
- Connect protocol — Buf
- Smithy — AWS API IDL
- Smithy GitHub — smithy-lang/smithy
- TypeSpec — Microsoft
- TypeSpec GitHub — microsoft/typespec
- Pact docs
- PactFlow
- Stoplight Elements
- Scalar — API docs
- Redocly
- Spectral — OpenAPI/AsyncAPI linter
- oasdiff — OpenAPI diff
- OpenAI — function calling
- Anthropic — tool use
- Gemini — function calling
- openapi-typescript
- openapi-generator