- Published on
OpenAI Responses API 422 스키마 검증 에러 해결 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버는 살아 있고 인증도 정상인데, 요청만 보내면 422 Unprocessable Entity가 떨어질 때가 있습니다. 특히 OpenAI Responses API를 붙여놓고 프롬프트/툴/멀티모달 입력을 조금만 복잡하게 만들면, “스키마 검증(schema validation)” 단계에서 바로 막히는 경우가 많습니다.
422는 단순히 “요청이 잘못됨”이 아니라 서버가 JSON은 파싱했지만, API가 기대하는 스키마와 맞지 않아 처리할 수 없음을 의미합니다. 즉, 네트워크/권한/레이트리밋 문제가 아니라 요청 바디의 구조·타입·필수 필드에서 삐끗한 겁니다.
이 글에서는 현업에서 자주 밟는 422 패턴을 재현 코드 + 수정 코드로 정리하고, 운영 환경에서 422를 “사전에 차단”하는 베스트 프랙티스까지 다룹니다.
422를 먼저 제대로 읽는 법
Responses API의 422는 보통 다음 형태로 옵니다(표현은 SDK/언어에 따라 조금 다름).
error.type:invalid_request_error또는 유사 타입error.message: 어떤 필드가 어떤 타입/구조를 기대했는지error.param: 문제가 된 경로(예:input[0].content[1].type)
핵심은 **에러 메시지의 “경로(path)”**입니다.
input배열의 몇 번째 요소인지- 그 요소의
content배열의 몇 번째 블록인지 - 그 블록의
type/text/image_url같은 필드 중 무엇이 문제인지
이 경로를 기준으로 “내가 만든 JSON”을 그대로 출력해 비교하면 대부분 잡힙니다.
가장 흔한 원인 1: input을 문자열로 보내거나, content를 문자열로 보내는 실수
Responses API는 input을 단순 문자열로도 받는 모드가 있지만(단일 텍스트), 멀티턴/멀티모달/role 기반으로 가면 보통 아래처럼 배열 + 객체 + content 블록 구조를 사용합니다.
실패 예시: content를 문자열로 보냄
from openai import OpenAI
client = OpenAI()
# ❌ content는 보통 배열(블록들)이어야 하는데 문자열로 보냄
resp = client.responses.create(
model="gpt-4.1-mini",
input=[
{
"role": "user",
"content": "Summarize this text in 3 bullets." # <- 422 유발 가능
}
],
)
print(resp.output_text)
수정 예시: content를 블록 배열로 보냄
from openai import OpenAI
client = OpenAI()
resp = client.responses.create(
model="gpt-4.1-mini",
input=[
{
"role": "user",
"content": [
{"type": "input_text", "text": "Summarize this text in 3 bullets."}
],
}
],
)
print(resp.output_text)
체크포인트
content는 문자열이 아니라 블록 배열로 보내는 습관을 들이면 422를 크게 줄일 수 있습니다.- 특히 이미지/파일/툴 호출이 섞이면 문자열
content는 거의 항상 함정이 됩니다.
가장 흔한 원인 2: content 블록의 type 오타 또는 필드명 불일치
블록 기반 입력에서 422가 나는 1순위는 type 값 오타입니다.
실패 예시: type을 text로 착각
# ❌ type은 "text"가 아니라 "input_text"가 필요한 경우가 많음
resp = client.responses.create(
model="gpt-4.1-mini",
input=[{
"role": "user",
"content": [
{"type": "text", "text": "Hello"}
]
}],
)
수정 예시
resp = client.responses.create(
model="gpt-4.1-mini",
input=[{
"role": "user",
"content": [
{"type": "input_text", "text": "Hello"}
]
}],
)
현업 팁: 팀 내에서 type 문자열을 여기저기 흩뿌려 쓰면 오타가 필연입니다. 아래처럼 상수화하거나(또는 Pydantic/JSON Schema로 강제) 관리하세요.
가장 흔한 원인 3: JSON은 맞는데 타입이 미묘하게 다름 (숫자/불리언/널)
422는 “문자열로 와야 하는데 숫자로 옴”, “배열이어야 하는데 객체로 옴” 같은 타입 불일치에서 잘 터집니다.
예를 들어, 메타데이터를 붙이면서 실수로 metadata를 문자열로 보내거나, max_output_tokens에 문자열을 넣는 식입니다.
# ❌ max_output_tokens는 int가 기대되는데 "512" 문자열을 넣음
resp = client.responses.create(
model="gpt-4.1-mini",
input="Write a haiku.",
max_output_tokens="512",
)
수정:
resp = client.responses.create(
model="gpt-4.1-mini",
input="Write a haiku.",
max_output_tokens=512,
)
체크포인트
- 환경변수/설정파일에서 읽어온 값은 기본적으로 문자열입니다. 숫자/불리언은 반드시 캐스팅하세요.
가장 흔한 원인 4: tool 정의 스키마가 JSON Schema가 아님
툴 호출을 붙이면 422가 급증합니다. 이유는 간단합니다.
tools[].function.parameters는 JSON Schema 형태여야 함required,properties,type등이 어긋나면 스키마 검증에서 컷
실패 예시: parameters에 Pydantic 스타일을 그대로 넣음
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather",
# ❌ JSON Schema가 아닌 임의 구조
"parameters": {
"city": "string",
"unit": "celsius"
},
},
}
]
수정 예시: JSON Schema로 명시
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {"type": "string", "enum": ["c", "f"]},
},
"required": ["city"],
"additionalProperties": False,
},
},
}
]
resp = client.responses.create(
model="gpt-4.1-mini",
input=[{
"role": "user",
"content": [{"type": "input_text", "text": "Seoul weather?"}],
}],
tools=tools,
)
베스트 프랙티스
additionalProperties: False를 넣으면 모델이 엉뚱한 필드를 뱉는 경우를 줄이고, 서버/클라이언트 양쪽에서 디버깅이 쉬워집니다.
가장 흔한 원인 5: 멀티모달(image) 블록 구조 오류
이미지 입력에서 422는 대개 image_url 구조를 잘못 맞춘 경우입니다.
실패 예시: image_url을 문자열로 넣음
resp = client.responses.create(
model="gpt-4.1-mini",
input=[{
"role": "user",
"content": [
{"type": "input_text", "text": "What is in this image?"},
# ❌ image_url은 보통 객체 형태를 기대
{"type": "input_image", "image_url": "https://example.com/a.png"},
],
}],
)
수정 예시: image_url 객체로
resp = client.responses.create(
model="gpt-4.1-mini",
input=[{
"role": "user",
"content": [
{"type": "input_text", "text": "What is in this image?"},
{"type": "input_image", "image_url": {"url": "https://example.com/a.png"}},
],
}],
)
이미지/파일을 크게 다루다 보면 422가 아니라 413(용량 제한)도 자주 맞습니다. 그때는 업로드/청크 전략을 따로 잡아야 합니다. OpenAI Responses API 413 에러 업로드 용량 제한과 청크 전략도 함께 참고하면 좋습니다.
트러블슈팅: 422를 5분 안에 잡는 체크리스트
1) “내가 보낸 JSON 원문”을 로그로 남겨라
422는 재현이 쉬운 대신, 요청 바디를 정확히 봐야 합니다.
- SDK 객체를 그대로 출력하지 말고
- 최종 직렬화된 JSON을 출력/저장하세요.
2) 에러의 param 경로를 그대로 따라가라
예: input[0].content[1].image_url.url이 문제라면
content[1]이 진짜 이미지 블록인지image_url이 dict인지url키가 존재하는지
를 그대로 점검하면 됩니다.
3) 400과 422를 구분하라
- 400: 필수 파라미터 누락, 모델명 오타, 지원 안 되는 조합 등 “요청 자체가 invalid”
- 422: JSON은 valid, 하지만 스키마가 invalid
400의 대표 원인/수정은 따로 정리해 둔 글이 도움이 됩니다: OpenAI Responses API 400 invalid_request_error 원인과 해결
Best Practice: Pydantic으로 ‘요청 스키마’를 로컬에서 먼저 검증하기
운영에서 422가 무서운 이유는, 릴리즈 후 특정 입력에서만 터져서 장애로 이어지기 때문입니다. 가장 좋은 패턴은 서버로 보내기 전에 로컬에서 스키마를 강제하는 겁니다.
아래는 “content 블록은 input_text 또는 input_image만 허용” 같은 정책을 Pydantic v2로 묶어 사전 검증하는 예시입니다.
from typing import List, Literal, Union, Optional
from pydantic import BaseModel, Field, HttpUrl
class InputTextBlock(BaseModel):
type: Literal["input_text"]
text: str = Field(min_length=1)
class ImageUrl(BaseModel):
url: HttpUrl
class InputImageBlock(BaseModel):
type: Literal["input_image"]
image_url: ImageUrl
ContentBlock = Union[InputTextBlock, InputImageBlock]
class InputMessage(BaseModel):
role: Literal["user", "developer", "system"]
content: List[ContentBlock]
class ResponsesRequest(BaseModel):
model: str
input: List[InputMessage]
max_output_tokens: Optional[int] = None
payload = ResponsesRequest(
model="gpt-4.1-mini",
input=[
InputMessage(
role="user",
content=[
InputTextBlock(type="input_text", text="Describe this image"),
InputImageBlock(type="input_image", image_url=ImageUrl(url="https://example.com/a.png")),
],
)
],
max_output_tokens=300,
)
# SDK에 넘길 dict는 검증된 값
data = payload.model_dump(mode="json")
이렇게 하면
type오타image_url구조 실수- 빈 문자열 텍스트
- max_output_tokens 타입 오류
같은 문제가 API 호출 전에 예외로 잡힙니다.
Pydantic v2 검증 에러를 현장에서 빠르게 해석하고 고치는 방법은 아래 글의 패턴이 그대로 적용됩니다: Pydantic v2 FastAPI 응답 검증 에러 7종 해결법
운영 관점: 422는 “재시도”로 해결되지 않는다
500/503처럼 일시 장애는 재시도·폴백·서킷브레이커로 흡수할 수 있지만, 422는 결정적(deterministic) 에러라 재시도해도 똑같이 실패합니다.
따라서 422 대응은 다음 순서가 효율적입니다.
- 로컬 스키마 검증(Pydantic/JSON Schema)
- 실패 시 요청 바디 + param 경로 로깅
- 사용자 입력/애플리케이션 상태에 따라 “수정 가능한” 케이스면 정규화(normalization)
- 수정 불가면 즉시 4xx로 반환(불필요한 재시도 금지)
(참고로 500/503 쪽은 재시도 설계가 핵심이므로 성격이 완전히 다릅니다. 필요하면 OpenAI Responses API 500·503 대응 재시도 폴백 서킷브레이커처럼 분리해서 설계하세요.)
결론: 422를 없애는 가장 현실적인 방법
- 422는 “API가 다운”이 아니라 내 요청의 스키마가 틀린 것입니다.
- 가장 많이 터지는 곳은
input/content구조, 블록type오타, 이미지/툴 스키마입니다. - 운영에서 422를 줄이려면 요청 페이로드를 Pydantic으로 사전 검증하고, 실패 시 문제 경로(param)와 원문 JSON을 남기는 체계를 만드세요.
지금 당장 할 일은 간단합니다.
- 현재 422가 나는 요청을 “최종 JSON”으로 덤프해 저장하고,
- 이 글의 체크리스트대로 경로를 따라가며 구조를 바로잡은 뒤,
- 같은 실수가 재발하지 않게 Pydantic 모델로 요청 스키마를 고정하세요.
이 3단계만 적용해도 Responses API 연동에서 가장 성가신 422의 80%는 사라집니다.