Skip to content
블로그로 돌아가기
튜토리얼

JSON을 Python으로: dataclass·Pydantic·TypedDict 가이드(2026)

JSON을 Python 클래스로 변환하는 온라인 가이드. dataclass·Pydantic v2·TypedDict 3가지 모드, Optional 타입, camelCase 별칭과 흔한 함정까지. 무료·브라우저 실행.

12 분 소요

JSON을 Python으로: dataclass·Pydantic·TypedDict 가이드(2026)

JSON을 JSON→Python 변환기에 붙여넣고 생성된 클래스를 복사하세요. JSON을 Python으로 옮기는 가장 빠른 길입니다. 설치할 것도 없고, 데이터는 어디로도 업로드되지 않습니다. 모든 처리가 브라우저 안에서 끝납니다. 한 번 쓰고 말 모델이나 낯선 API 응답을 잠깐 들여다볼 때라면 몇 초면 끝납니다.

어려운 부분은 변환기가 대신 풀어 주지 못합니다. 타입은 정확하게 추론해 주지만, 판단이 필요한 지점은 남겨 둡니다. 세 가지 출력 중 무엇이 필요한지입니다. dataclass, Pydantic v2의 BaseModel, TypedDict는 완전히 똑같은 JSON을 표현할 수 있지만, 타입 안전성의 강도, 런타임 동작, 데이터를 적재하는 방식에서 서로 다릅니다. 잘못 고르면 쓸데없이 Pydantic 의존성을 짊어지거나, dataclass가 입력을 검사해 줄 거라 믿었는데 실제로는 조용히 아무 검사도 하지 않는 상황에 놓입니다.

이 가이드에서는 세 가지 모두에 대한 선택 기준표, 변환기가 타입을 추론하는 방식, 모드별 camelCase 처리, 생성한 클래스로 JSON을 파싱하는 방법(많은 사람이 걸려 넘어지는 중첩 함정 포함), Pydantic v2와 v1의 차이, 그리고 출력을 믿고 쓰기 전에 알아 둘 만한 예외 상황을 다룹니다.

JSON을 Python으로 변환하는 방법

JSON을 Python으로 변환하는 과정은 세 단계입니다:

  1. JSON을 붙여넣으세요. 객체, 배열, 또는 가공하지 않은 API 응답을 입력창에 넣습니다. 변환은 즉시, 전부 클라이언트 측에서 이뤄집니다.
  2. dataclass, Pydantic, TypedDict 중에서 고르세요. Output 토글로 스타일을 바꾸고, 기본값인 RootUserApiResponse 같은 의미 있는 이름으로 바꿉니다.
  3. 복사하거나 내려받으세요. 생성된 Python을 클릭 한 번으로 가져와 models.py를 프로젝트에 그대로 넣습니다.

여기까지가 실제 작업의 전부입니다. 입력이 압축돼 있거나 유효한지 확신이 서지 않는다면, 먼저 JSON 포맷터를 거쳐 변환기가 형식이 잘 갖춰진 JSON을 읽도록 하세요. 이 가이드의 나머지는 그 출력을 설명해, 도구가 스스로 추론하지 못하는 부분을 직접 손볼 수 있게 합니다.

JSON 타입이 Python으로 매핑되는 방식

모든 JSON 값에는 대응하는 Python 타입이 있고, 그 대응 관계는 대체로 단순합니다:

JSON 값Python 타입
"text"str
42int (임의 정밀도, 오버플로 없음)
3.14, 2e3float
true / falsebool
nullOptional[T] / Optional[Any]
[1, 2, 3]List[T]
{ ... }이름 있는 클래스

전형적인 REST 페이로드를 예로 들어 보겠습니다:

{ "id": 101, "name": "Ada Lovelace", "email": "ada@example.com", "active": true, "roles": ["admin", "user"] }

dataclass 모드에서 변환기는 바로 쓸 수 있는 클래스를 생성합니다:

from dataclasses import dataclass
from typing import List


@dataclass
class Root:
    id: int
    name: str
    email: str
    active: bool
    roles: List[str]

이 대목에서 Python이 한 가지 유리합니다. Python의 int는 임의 정밀도라, Discord snowflake든 2^53을 넘는 어떤 ID든 손실 없이 그대로 int로 남습니다. 같은 값이라면 자바스크립트(JavaScript)는 정밀도를 잃고, Rust는 i64, u64, f64 중에서 고르게 만듭니다. 소수점이나 지수 표기로 쓴 토큰만 float로 추론됩니다. 오직 null이기만 한 필드는 샘플만으로는 타입을 정할 수 없어 Optional[Any]로 물러납니다. 값이 채워진 대표 페이로드를 붙여넣으면 Any보다 더 구체적인 타입을 얻습니다.

dataclass vs Pydantic v2 vs TypedDict — 무엇을 써야 할까?

변환기가 대신 정해 주지 못하고 여러분 몫으로 남기는 선택이며, 이 가이드도 여기에 초점을 둡니다. 의존성 없는 내부 데이터 보관용이 필요하면 dataclass, 신뢰할 수 없는 입력을 파싱하거나 검사한다면 Pydantic v2, 이미 가지고 있는 딕셔너리에 정적 타입 힌트만 얹고 싶다면 TypedDict를 고르세요.

구분dataclassPydantic v2TypedDict
의존성표준 라이브러리pip install pydantic표준 라이브러리 (typing)
런타임 유효성 검사없음있음 — 검사하고 강제 변환없음 — 힌트뿐
런타임 비용가벼움 (실제 객체)어느 정도 (유효성 검사)없음 (dict 그 자체)
중첩 자동 파싱없음, 수동있음, model_validate가 재귀 처리없음, dict일 뿐
camelCase 별칭원래 키 유지Field(alias=...)함수형 문법
적합한 용도내부 구조체API 응답, 웹훅, 설정레거시 딕셔너리 코드에 타입 부여

{"id": 101, "name": "Ada Lovelace", "active": true}처럼 작은 형태라면 세 가지 대상이 거의 똑같아 보입니다. JSON을 Python dataclass로 변환하면 표준 라이브러리 객체를 얻습니다:

from dataclasses import dataclass


@dataclass
class Root:
    id: int
    name: str
    active: bool

JSON을 Pydantic으로 변환한 버전은 BaseModel로 바꿔 끼웁니다. 비슷해 보이지만 런타임에서 제 몫을 해냅니다:

from pydantic import BaseModel


class Root(BaseModel):
    id: int
    name: str
    active: bool

그리고 JSON에서 만든 Python TypedDict는 새로 할당하는 것 없이 평범한 딕셔너리의 형태만 설명합니다:

from typing import TypedDict


class Root(TypedDict):
    id: int
    name: str
    active: bool

같은 필드지만 계약은 셋 다 전혀 다릅니다. 하나의 샘플에서 세 가지를 모두 생성하는 값어치는, 도구 안에서 이들을 오가며 작업에 맞는 모드를 고를 수 있다는 데 있습니다. 통제할 수 없는 데이터를 파싱할 때는 Pydantic, 데이터가 내부용일 때는 dataclass, 이미 여기저기 넘겨 쓰는 딕셔너리를 mypy가 이해하게만 하고 싶을 때는 TypedDict입니다.

변환기가 클래스를 추론하는 방식

세 가지 규칙이 여러분이 넣는 거의 모든 입력을 감당합니다. 객체 형태마다 클래스 하나, 배열은 키 단위 병합, 그리고 누락된 값의 신중한 처리입니다.

구조 추론: 객체마다 이름 있는 클래스 하나

서로 다른 객체 형태는 저마다 이름 있는 클래스가 됩니다. 중첩 객체는 인라인으로 넣지 않고, 별도의 참조되는 정의로 끌어올립니다:

{ "repo": "pydantic", "owner": { "login": "samuelcolvin", "id": 100 } }
from dataclasses import dataclass


@dataclass
class Owner:
    login: str
    id: int


@dataclass
class Root:
    repo: str
    owner: Owner

중첩된 owner는 자체 Owner 클래스가 되어 필드로 참조됩니다. 순서를 눈여겨보세요. 자식 클래스가 그것을 쓰는 부모보다 먼저 나오므로, from __future__ import annotations 없이도 모듈이 실행됩니다. 동일한 형태는 중복 제거까지 되어, 구조가 같은 두 필드는 사본을 만드는 대신 하나의 클래스를 공유합니다.

배열 병합과 Optional 필드

객체 배열을 넘기면 변환기는 이들을 키 단위로 병합해 하나의 요소 타입으로 만듭니다. 어떤 요소에는 있지만 다른 요소에는 없는 키는 Optional이 됩니다:

{ "users": [{ "id": 1, "nick": "x" }, { "id": 2 }] }
from typing import List, Optional, TypedDict


class User(TypedDict):
    id: int
    nick: Optional[str]


class Root(TypedDict):
    users: List[User]

id는 모든 요소에 나타나므로 필수로 남습니다. nick은 두 번째 사용자에 없으므로 Optional[str]이 됩니다. 객체 하나가 빈약한 샘플인 이유가 바로 이것입니다. 도구는 어딘가에서 키가 실제로 빠진 것을 봤을 때만 필드를 Optional로 표시할 수 있습니다. 대표성 있는 배열을 넣으면 추론이 실제 형태를 반영합니다.

Optional, None, 그리고 빈 배열

몇몇 값은 그 자체로는 타입을 담고 있지 않고, 변환기는 이 점을 정직하게 드러냅니다. 일부 배열 항목에서 빠진 키는 Optional[T]가 됩니다. 오직 null이기만 한 필드는 Optional[Any]가 되는데, JSON null 하나만으로는 의도한 타입에 대해 아무것도 알 수 없기 때문입니다. 빈 배열이나 요소 타입이 뒤섞인 배열은 List[Any]로 물러납니다. 이 중 어느 것도 버그가 아니라, 도구가 추측을 삼가는 것입니다. 타입을 보여 주는 샘플이 생기면 각 Any를 구체적인 타입으로 바꾸세요. 그리고 Python의 임의 정밀도 int 덕분에 큰 ID가 Rust나 자바스크립트에서처럼 더 넓은 숫자 타입을 강요하는 일은 절대 없다는 점을 기억하세요.

모드별 camelCase 키 처리

JSON 키는 흔히 camelCase인데 관용적인 Python 필드는 snake_case입니다. 여기에는 정답이 하나로 정해져 있지 않아 모드마다 다르게 해결하며, 출력을 복사하기 전에 그 차이를 이해해 둘 만합니다.

dataclass. dataclass에는 내장된 별칭 메커니즘이 없습니다. Root(**data)가 계속 동작하도록, 변환기는 원래 키가 유효한 Python 식별자이기만 하면 그것을 필드 이름으로 유지합니다. 그래서 publicRepospublicRepos 그대로 남습니다. 억지로 snake_case로 바꾸면 **dataTypeError로 깨집니다.

Pydantic v2. 여기서는 관용적인 결과를 얻습니다. 필드는 snake_case로 이름이 바뀌고, Field(alias=...)가 정확한 JSON 키로 다시 매핑됩니다. 여기에 model_config = ConfigDict(populate_by_name=True)가 더해져 필드 이름으로도, 별칭으로도 모델을 만들 수 있습니다:

from pydantic import BaseModel, ConfigDict, Field


class Root(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    login: str
    public_repos: int = Field(alias="publicRepos")
    created_at: str = Field(alias="createdAt")

덕분에 Root.model_validate(api_json)가 실제 camelCase 페이로드를 왕복 처리하면서도 코드는 자연스럽게 읽힙니다.

TypedDict. 평범한 식별자 키는 그대로 씁니다. 식별자가 아닌 키가 나타나는 순간, 변환기는 정확한 키를 보존하기 위해 함수형 TypedDict('User', {...}) 문법으로 전환합니다. 이는 다음 절에서 다룹니다.

Python 키워드와 식별자가 아닌 키

JSON 키는 그저 문자열이라, API가 class, from, first-name을 보내는 것을 막을 방법이 없습니다. 이들은 Python 속성 이름으로 그대로 쓰면 문법 오류가 나므로, 변환기가 정리(sanitize)해 줍니다.

Python 키워드인 키에는 뒤에 밑줄이 붙습니다. classclass_, fromfrom_이 됩니다. Pydantic 모드에서는 Field(alias=...)도 함께 유지해, 모델이 원래의 전송 키를 그대로 읽습니다:

from pydantic import BaseModel, ConfigDict, Field


class Root(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    class_: str = Field(alias="class")
    from_: str = Field(alias="from")

하이픈, 공백, 또는 맨 앞의 숫자가 들어간 키는 dataclass와 Pydantic 모드에서 유효한 식별자로 정리됩니다. TypedDict는 리터럴 키를 그대로 유지할 수 있는 유일한 모드로, 함수형 형태를 써서 "first-name"이 손대지 않은 채 살아남습니다:

from typing import TypedDict

Root = TypedDict("Root", {"first-name": str, "2fa": bool})

결과는 언제나 실행되며, TypedDict 모드에서는 키가 API가 보낸 바이트 그대로 남습니다.

생성한 클래스로 JSON 파싱하기

클래스를 생성하는 것은 절반의 일입니다. 거기에 JSON을 적재하는 단계에서 세 모드가 뚜렷하게 갈리고, 널리 퍼진 한 가지 가정이 어긋납니다.

dataclass. 평평한 객체라면 json.loadsRoot(**data)를 더하면 곧바로 동작합니다:

import json
from dataclasses import dataclass
from typing import List


@dataclass
class Root:
    id: int
    name: str
    roles: List[str]


data = json.loads('{"id": 101, "name": "Ada Lovelace", "roles": ["admin"]}')
root = Root(**data)
print(root.name, root.roles)

하지만 dataclass는 재귀하지 않습니다. JSON에 중첩 객체가 있으면 Root(**data)는 최상위 계층만 채우고, 모든 자식은 여러분이 생성한 Owner 클래스가 아니라 평범한 dict로 남겨 둡니다. 트리 전체를 파싱하려면 자식을 직접 만들거나, dacitepydantic.dataclasses 같은 라이브러리를 쓰거나, 도구를 Pydantic 모드로 바꾸세요. 이것이 대부분의 변환기가 결코 언급하지 않는 함정입니다.

Pydantic v2. 한 번의 호출로 트리 전체를 파싱하고 유효성을 검사합니다:

import json
from pydantic import BaseModel


class Root(BaseModel):
    id: int
    name: str


root = Root.model_validate(json.loads('{"id": 101, "name": "Ada Lovelace"}'))
print(root.name)

타입이 맞지 않으면 조용히 통과하는 대신 명확한 ValidationError를 던집니다. 이것이 의존성을 짊어진 값어치입니다. 모델이 런타임 유효성 검사기이므로, 값을 반환한 model_validate 호출은 데이터가 실제로 맞았다는 보장입니다. 타입 검사기가 맞을 거라고 가정하는 것과는 다릅니다.

TypedDict. 런타임에서 TypedDict는 평범한 dict입니다. data: Root = json.loads(text)라고 주석을 다는 것은 순전히 정적 검사기를 위한 것으로, 코드가 실행될 때는 아무것도 검사되지 않습니다. 진짜 강제가 필요하면 CI에서 mypy를 돌리거나 Pydantic으로 옮기세요. 모드를 고르는 일은 런타임 보장을 얼마나 원하는지를 정하는 일이며, 계약이 타입을 넘어서야 할 때는 JSON Schema 유효성 검사 가이드가 구조를 처음부터 끝까지 강제하는 방법을 다룹니다.

Pydantic v2 vs v1 — 무엇이 바뀌었나

변환기는 v2 문법을 내놓습니다. 아직 v1을 돌리는 코드베이스도 많은데, v2 출력을 v1 프로젝트에 붙여넣으면 로직이 아니라 이름이 바뀐 메서드에서 실패합니다. 매핑은 다음과 같습니다:

작업Pydantic v2 (도구 출력)Pydantic v1
딕셔너리 파싱model_validate(data)parse_obj(data)
가공하지 않은 JSON 파싱model_validate_json(text)parse_raw(text)
모델 설정model_config = ConfigDict(...)내부 class Config
딕셔너리로 직렬화model_dump()dict()
JSON으로 직렬화model_dump_json()json()

별칭 동작도 더 엄격해졌습니다. v2에서는 필드 이름으로든 별칭으로든 모델을 만들려면 Field(alias=...) 옆에 populate_by_name=True가 필요하며, 도구가 이를 포함하는 이유입니다. 실무에서는 간단합니다. 가능하면 v2로 올리세요. 더 빠르고 지금도 개발이 이어지는 쪽입니다. 그럴 수 없다면 model_validateparse_obj로, ConfigDict를 내부 Config로 되돌려 쓰세요.

json-to-python vs quicktype vs datamodel-code-generator vs 직접 작성

JSON에서 Python 클래스를 생성하는 단 하나의 최선책은 없습니다. JSON이 어디에 있는지, 그리고 무엇에서 출발하는지에 달려 있습니다.

방식적합한 용도비고
온라인 변환기 (이 도구, jsonlint, codeshack)한 번뿐인 변환, 민감한 페이로드, 설치 불필요샘플에서 추론, 완전히 클라이언트 측
quicktype여러 언어 출력, 파이프라인 코드 생성마찬가지로 샘플 기반. 별칭과 모드 선택 면에서 Python 지원은 다소 얕음
datamodel-code-generatorJSON 스키마나 OpenAPI로부터 Pydantic 생성입력이 샘플이 아니라 스키마 — 있을 때 더 권위 있음
손으로 클래스 작성아주 작은 페이로드, 문법 익히기완전한 제어, 하지만 번거롭고 어긋나기 쉬움

구분의 핵심은 입력입니다. 온라인 변환기와 quicktype은 둘 다 JSON 샘플에서 타입을 추론하지만, datamodel-code-generator는 JSON Schema나 OpenAPI 명세를 근거로 읽습니다. 탐색과 한 번뿐인 작업에는 샘플 기반 도구를 쓰고, 계약이 이미 존재할 때는 스키마 기반 도구를 꺼내세요. 코드베이스가 Python이 아니라 TypeScript라면 JSON to TypeScript 변환기로 같은 샘플 기반 방식을 쓸 수 있고, JSON to TypeScript 인터페이스 가이드가 그쪽을 깊이 있게 다룹니다. 강타입 컴파일 언어에서의 런타임 유효성 검사 관점이라면 JSON to Rust 구조체 가이드가 유용한 대조가 됩니다. serde는 Rust의 Pydantic이니까요.

JSON에서 Python을 생성할 때 흔한 함정

생성된 클래스는 출발점입니다. 실제 데이터로 출력에 의존하기 전에 다음을 주의하세요.

  • 샘플 하나로는 모든 형태가 드러나는 일이 드뭅니다. Optional은 실제로 서로 다른 배열 요소에서만 추론할 수 있습니다. 운 좋은 객체 하나에서 나온 추측이 아니라 정확한 옵셔널이 되도록, 대표성 있는 배열을 붙여넣으세요.
  • dataclass는 유효성을 검사하지 않습니다. Root(**data)는 필드를 대입할 뿐, 어떤 타입도 검사하지 않고 아무것도 재귀하지 않습니다. 강제가 필요하면 Pydantic을 쓰세요.
  • 중첩 JSON은 Root(**data)를 깨뜨립니다. 최상위 계층만 채워지고 자식은 딕셔너리로 남습니다. Pydantic의 model_validate로 바꾸거나 dacite를 쓰세요.
  • 항상 null인 필드는 Optional[Any]가 됩니다. 이를 평범한 옵셔널이 아니라 자리표시자로 여기고, 실제 타입을 알게 되면 부여하세요.
  • v1 프로젝트에 들어간 v2 출력. model_validate가 정의되지 않은 상태가 됩니다. v2로 올리거나 parse_obj로 다시 쓰세요.
  • str로 남겨 둔 날짜로는 날짜 계산을 할 수 없습니다. 필드를 datetime으로 바꾸거나, Pydantic이라면 datetime 필드를 써서 ISO 8601을 대신 파싱하게 하세요.

자주 묻는 질문

JSON에는 dataclass, Pydantic, TypedDict 중 무엇을 써야 하나요?

의존성 없는 내부 데이터 보관용에는 dataclass를 쓰세요. 신뢰할 수 없는 입력을 파싱하거나 검사할 때는 Pydantic v2를 쓰세요. 런타임에 타입을 강제하기 때문입니다. 이미 가지고 있는 딕셔너리에 런타임 비용 없이 정적 힌트를 더하려면 TypedDict를 쓰세요. 데이터가 통제 밖에서 온다면 기본값으로 Pydantic을 고르세요.

Pydantic은 런타임에 JSON을 검사하나요?

그렇습니다. Root.model_validate(data)는 코드가 실행될 때 타입을 검사하고 강제 변환하며, 잘못된 입력에는 ValidationError를 던집니다. dataclass는 둘 다 하지 않습니다 — Root(**data)는 필드를 대입할 뿐입니다. TypedDict도 둘 다 하지 않습니다. mypy 같은 정적 검사기를 위한 힌트일 뿐, 런타임에는 평범한 딕셔너리로 동작합니다.

JSON이 가끔 생략하는 필드는 어떻게 타입을 지정하나요?

Optional[T]로 지정하세요. 키가 일부 배열 요소에만 나타나면 변환기가 자동으로 Optional로 표시합니다. Pydantic에서는 = None 같은 기본값을 필드에 주어, 키가 없어도 유효성 검사 오류가 나지 않게 하세요. dataclass 필드도 field(default=None)으로 같은 방식으로 기본값을 줄 수 있습니다.

제 dataclass는 왜 중첩 JSON 파싱에 실패했나요?

Root(**data)가 재귀하지 않기 때문입니다. 최상위 필드는 채우지만, 중첩 객체는 여러분이 생성한 자식 클래스가 아니라 평범한 딕셔너리로 남깁니다. 한 번의 호출로 트리 전체를 파싱하려면 Pydantic의 model_validate로 바꾸거나, dacite나 pydantic.dataclasses로 재귀를 더하세요.

큰 JSON 정수는 어떤 Python 타입이 되나요?

int입니다. Python 정수는 임의 정밀도라, snowflake든 2^53을 넘는 Discord ID든 손실 없이 그대로 int로 남습니다. 자바스크립트에 있는 정밀도 손실을 겪을 일이 없고, Rust가 강요하는 것처럼 i64u64 사이에서 고를 일도 없습니다.

camelCase JSON 키는 모드별로 어떻게 처리되나요?

dataclass는 원래 키를 유지해 Root(**data)가 계속 동작합니다. Pydantic은 Field(alias="camelKey")populate_by_name=True로 필드를 snake_case로 바꿔, 실제 API 데이터를 왕복 처리합니다. TypedDict는 함수형 TypedDict('Name', {...}) 문법을 써서 유효한 식별자가 아닌 어떤 키든 보존합니다.

class처럼 Python 키워드인 JSON 키는 어떻게 처리하나요?

변환기가 뒤에 밑줄을 붙여 classclass_로 바꾸므로 코드가 유효해집니다. Pydantic 모드에서는 Field(alias="class")도 더해 필드를 원래 전송 키로 다시 매핑하므로, 이름은 적법하면서도 모델은 실제 페이로드를 그대로 읽습니다.

온라인 JSON to Python 변환기를 쓸 때 제 JSON은 비공개인가요?

그렇습니다. 변환은 자바스크립트로 브라우저 안에서 100% 이뤄집니다. 토큰, ID, 고객 데이터를 포함한 여러분의 JSON은 페이지를 결코 벗어나지 않으며 서버로 전송되지도 않습니다. 페이지가 한 번 로드되면 오프라인에서도 작동합니다. 준비되면 여러분의 페이로드로 JSON to Python 변환기를 사용해 보세요.

태그: python json pydantic type-safety developer-tools