Rust Serde로 JSON 다루기
JSON은 어디에나 있어요. 설정 파일, 웹 API, 게임 저장 데이터까지요. Rust에서는 거의 모든 사람이 JSON을 다룰 때 크레이트 두 개를 써요. serde(serialize / deserialize의 줄임말)는 Rust 값을 데이터로 바꾸고 다시 되돌리는 방법을 알아요. serde_json 크레이트는 거기에 JSON 형식을 가르쳐 줘요.
serde를 통역사라고 생각해 보세요. 데이터를 평범한 Rust 구조체로 한 번만 설명해 두면, serde가 JSON으로 바꾸고 다시 되돌리는 일을 대신 해 줘요. JSON 텍스트를 손으로 쓸 일도, JSON을 한 글자씩 뜯어볼 일도 없어요.
준비하기
섹션 제목: “준비하기”프로젝트 안에서 두 크레이트를 추가하세요. derive 기능을 켜면 serde가 변환 코드를 대신 만들어 줘요.
cargo add serde --features derivecargo add serde_json이제 Cargo.toml에 이런 내용이 생겨요.
[dependencies]serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"Cargo.toml과 cargo add가 처음이라면 Cargo와 크레이트를 먼저 읽어 보세요. Rust Playground에는 두 크레이트가 이미 들어 있어서, 이 페이지의 “직접 실행해 보기” 버튼은 아무 준비 없이 모두 동작해요.
구조체를 JSON으로
섹션 제목: “구조체를 JSON으로”구조체에 #[derive(Serialize)]를 붙이면 serde_json::to_string으로 JSON 텍스트를 만들 수 있어요.
use serde::Serialize;
#[derive(Serialize)]struct User { name: String, age: u32, admin: bool,}
fn main() { let user = User { name: String::from("Ana"), age: 28, admin: false };
let compact = serde_json::to_string(&user).unwrap(); println!("{}", compact);
let pretty = serde_json::to_string_pretty(&user).unwrap(); println!("{}", pretty);}실행 결과:
{"name":"Ana","age":28,"admin":false}{ "name": "Ana", "age": 28, "admin": false}to_string은 빈칸 없는 압축된 JSON을 만들어요. 네트워크로 보낼 때 좋아요. to_string_pretty는 줄바꿈과 공백을 넣어 줘서, 사람이 읽을 파일에 더 잘 어울려요. 키는 구조체의 필드 순서를 그대로 따라가요.
JSON을 구조체로
섹션 제목: “JSON을 구조체로”반대 방향을 역직렬화(deserializing)라고 해요. derive 목록에 Deserialize를 추가하고 serde_json::from_str을 호출하세요. 어떤 타입을 원하는지는 타입 표기로 알려 줘요.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]struct User { name: String, age: u32, admin: bool,}
fn main() -> Result<(), serde_json::Error> { let text = r#"{"name": "Ana", "age": 28, "admin": false}"#;
let user: User = serde_json::from_str(text)?; println!("{:?}", user); println!("{}의 나이는 {}살이에요", user.name, user.age); Ok(())}실행 결과:
User { name: "Ana", age: 28, admin: false }Ana의 나이는 28살이에요두 가지를 눈여겨보세요.
r#"..."#는 원시 문자열(raw string)이에요. 안에서"를 마음껏 쓸 수 있어서 JSON을 쓸 때 아주 편해요.- 텍스트가 올바른 JSON이 아닐 수도 있어서
from_str은Result를 돌려줘요. 여기서는main이Result를 돌려주니까?로 에러를 위로 넘길 수 있어요.
JSON이 잘못됐을 때
섹션 제목: “JSON이 잘못됐을 때”실제 JSON은 기대한 모양이 아닐 때가 많아요. 필드가 빠져 있거나, 숫자가 텍스트로 들어오기도 해요. serde는 모든 걸 검사하고, 프로그램을 죽이는 대신 알기 쉬운 에러를 줘요. match로 처리해 보세요.
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct User { name: String, age: u32,}
fn main() { let inputs = [ r#"{"name": "Ana", "age": 28}"#, r#"{"name": "Ben"}"#, r#"{"name": "Cy", "age": "old"}"#, ];
for text in inputs { match serde_json::from_str::<User>(text) { Ok(user) => println!("OK: {:?}", user), Err(e) => println!("에러: {}", e), } }}실행 결과:
OK: User { name: "Ana", age: 28 }에러: missing field `age` at line 1 column 15에러: invalid type: string "old", expected u32 at line 1 column 27에러 메시지는 serde가 만든 것이라 영어로 나와요. 첫 번째는 “age 필드가 없어요(1번째 줄 15번째 칸)”, 두 번째는 “타입이 틀렸어요. u32를 기대했는데 문자열 "old"가 왔어요”라는 뜻이에요. 칸 번호가 바이트 위치로 계산되기 때문에, 입력 JSON은 영어 그대로 두었어요.
::<User> 부분은 “터보피시”(turbofish)라고 불러요. 원하는 타입을 알려 주는 또 다른 방법인데, let user: User처럼 타입을 붙일 자리가 없을 때 유용해요.
빠진 키: Option과 #[serde(default)]
섹션 제목: “빠진 키: Option과 #[serde(default)]”필드가 정말로 있어도 되고 없어도 되는 거라면 어떻게 할까요? 좋은 방법이 두 가지 있어요.
- 필드를
Option으로 만들어요. 키가 없거나null이면None이 돼요. #[serde(default)]를 붙여요. 키가 없으면 그 타입의 기본값이 들어가요.0,false, 빈String, 빈Vec같은 것들이에요.
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct Profile { name: String, bio: Option<String>, #[serde(default)] followers: u32, #[serde(default)] tags: Vec<String>,}
fn main() { let full = r#"{"name": "Ana", "bio": "Rust를 좋아해요", "followers": 120, "tags": ["rust"]}"#; let short = r#"{"name": "Ben"}"#;
let a: Profile = serde_json::from_str(full).unwrap(); let b: Profile = serde_json::from_str(short).unwrap();
println!("{:?}", a); println!("{:?}", b);}실행 결과:
Profile { name: "Ana", bio: Some("Rust를 좋아해요"), followers: 120, tags: ["rust"] }Profile { name: "Ben", bio: None, followers: 0, tags: [] }“값이 없음”이 “0”이나 “비어 있음”과 다른 의미라면 Option을 쓰세요. 기본값으로 충분하다면 default를 쓰세요.
필드 이름 바꾸기
섹션 제목: “필드 이름 바꾸기”Rust는 필드 이름에 snake_case를 쓰지만, 많은 웹 API는 camelCase를 쓰고, 어떤 API는 Rust에서 아예 쓸 수 없는 이름(type 같은)을 쓰기도 해요. 그렇다고 깔끔한 Rust 이름을 포기할 필요는 없어요. 구조체 전체에는 rename_all을, 필드 하나에는 rename을 쓰세요.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]#[serde(rename_all = "camelCase")]struct UserProfile { user_id: u64, display_name: String, is_verified: bool, #[serde(rename = "type")] account_type: String,}
fn main() { // 웹 API가 돌려줄 법한 응답 let response = r#"{ "userId": 42, "displayName": "Ana Silva", "isVerified": true, "type": "premium" }"#;
let profile: UserProfile = serde_json::from_str(response).unwrap(); println!("{}(id {})님은 {} 계정이에요", profile.display_name, profile.user_id, profile.account_type);
// 직렬화할 때도 같은 이름을 쓰니까, JSON이 원래 모양 그대로 돌아와요 println!("{}", serde_json::to_string(&profile).unwrap());}실행 결과:
Ana Silva(id 42)님은 premium 계정이에요{"userId":42,"displayName":"Ana Silva","isVerified":true,"type":"premium"}serde는 기본적으로 구조체에 없는 JSON 키를 그냥 무시해요. 그래서 API가 필드를 쉰 개나 보내도, 나에게 필요한 필드만 설명하면 돼요.
여러 개의 항목: 구조체의 Vec
섹션 제목: “여러 개의 항목: 구조체의 Vec”JSON 배열은 Vec이 돼요. 구조체 안에 다른 구조체나 벡터를 넣을 수도 있으니, 깊이 중첩된 JSON도 타입을 중첩하기만 하면 설명할 수 있어요.
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct Task { title: String, done: bool,}
fn main() { let text = r#"[ {"title": "serde 배우기", "done": true}, {"title": "설정 파일 쓰기", "done": false}, {"title": "API 호출하기", "done": false} ]"#;
let tasks: Vec<Task> = serde_json::from_str(text).unwrap();
for task in &tasks { let mark = if task.done { "x" } else { " " }; println!("[{}] {}", mark, task.title); } let left = tasks.iter().filter(|t| !t.done).count(); println!("남은 할 일: {}개", left);}실행 결과:
[x] serde 배우기[ ] 설정 파일 쓰기[ ] API 호출하기남은 할 일: 2개타입 없는 JSON: Value와 json!
섹션 제목: “타입 없는 JSON: Value와 json!”JSON의 모양을 미리 모르거나, 일부분만 필요할 때도 있어요. 그럴 때는 serde_json::Value를 쓰세요. 객체, 배열, 문자열, 숫자, 불리언, null 등 어떤 JSON이든 담을 수 있어요. json! 매크로를 쓰면 JSON과 비슷한 문법으로 Value를 만들 수 있어요.
use serde_json::{json, Value};
fn main() { let data: Value = json!({ "city": "서울", "temps": [18, 21, 19], "sunny": true });
println!("도시: {}", data["city"]); println!("첫 번째 기온: {}", data["temps"][0]);
// as_str()은 JSON 문자열 대신 평범한 &str을 줘요 if let Some(city) = data["city"].as_str() { println!("그냥 텍스트: {}", city); }
// 없는 키는 프로그램을 죽이지 않고 Null을 줘요 println!("바람: {}", data["wind"]);}실행 결과:
도시: "서울"첫 번째 기온: 18그냥 텍스트: 서울바람: nulldata["city"]를 출력하면 따옴표가 붙는다는 점을 눈여겨보세요. 아직 JSON 문자열이기 때문이에요. 평범한 Rust 값을 얻으려면 .as_str(), .as_i64(), .as_bool()을 쓰세요. 값이 다른 타입일 수도 있으니, 모두 Option을 돌려줘요.
Value는 유연하지만 컴파일러의 도움을 받을 수 없어요. 모양을 알고 있다면 구조체를 쓰는 편이 좋아요.
JSON 파일 읽고 쓰기
섹션 제목: “JSON 파일 읽고 쓰기”실제로 쓸 만한 예제를 볼게요. 앱 설정을 config.json에 저장하고 다시 불러와요. 이 페이지 내용과 파일 입출력을 함께 써요. std::fs::write로 텍스트를 저장하고, std::fs::read_to_string으로 다시 읽어요.
use serde::{Deserialize, Serialize};use std::error::Error;use std::fs;
#[derive(Serialize, Deserialize, Debug)]struct Config { username: String, theme: String, font_size: u32,}
fn main() -> Result<(), Box<dyn Error>> { let config = Config { username: String::from("ana"), theme: String::from("dark"), font_size: 14, };
// 저장 let text = serde_json::to_string_pretty(&config)?; fs::write("config.json", text)?;
// 불러오기 let loaded_text = fs::read_to_string("config.json")?; let loaded: Config = serde_json::from_str(&loaded_text)?; println!("불러온 설정: {:?}", loaded); Ok(())}실행 결과:
불러온 설정: Config { username: "ana", theme: "dark", font_size: 14 }왜 Box<dyn Error>일까요? 이 main은 두 가지 방식으로 실패할 수 있어요. 파일 에러(std::io::Error)일 수도 있고, JSON 에러(serde_json::Error)일 수도 있어요. Box<dyn Error>는 “어떤 종류의 에러든”이라는 뜻이라서 ?가 둘 다에 동작해요. 자세한 내용은 사용자 정의 에러 페이지에서 설명해요.
자주 하는 실수
섹션 제목: “자주 하는 실수”Deserialize derive를 잊기. 구조체가 Serialize만 derive하면 JSON을 쓸 수는 있지만 읽을 수는 없어요. 에러 메시지가 길지만, 중요한 줄은 이거예요.
error[E0277]: the trait bound `User: serde::Deserialize<'de>` is not satisfied |10 | let user: User = serde_json::from_str(r#"{"name":"Ana","age":28}"#).unwrap(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound | = note: for local types consider adding `#[derive(serde::Deserialize)]` to your `User` type“User가 Deserialize 트레이트를 구현하지 않았으니 #[derive(serde::Deserialize)]를 추가해 보라”는 뜻이에요. 해결 방법은 #[derive(Serialize, Deserialize)]예요. 대신 “cannot find derive macro Serialize“가 보인다면, derive 기능 없이 serde를 추가한 거예요. cargo add serde --features derive를 실행하세요.
내가 통제할 수 없는 데이터에 .unwrap() 쓰기. 작은 예제에서는 괜찮지만, 사용자나 웹사이트에서 온 JSON은 깨져 있을 수 있어요. 잘못된 파일 하나에 프로그램이 패닉으로 죽어요. 위에서 본 것처럼 ?나 match를 쓰세요.
data["name"]이 따옴표 없이 출력되길 기대하기. Value는 JSON 형태로 출력돼요. 평범한 텍스트가 필요하면 .as_str()을 호출하세요.
연습 문제
섹션 제목: “연습 문제”1. 장보기 목록 저장하기
섹션 제목: “1. 장보기 목록 저장하기”name(String)과 qty(u32)를 가진 구조체 Item을 만드세요. Vec에 항목 두 개를 넣고, 호출 한 번으로 벡터 전체를 JSON으로 바꿔서 출력하세요.
힌트: #[derive(Serialize)]는 구조체에만 붙이면 돼요. 직렬화할 수 있는 것들의 Vec은 그 자체로 직렬화할 수 있고, JSON 배열이 돼요.
정답 보기
use serde::Serialize;
#[derive(Serialize)]struct Item { name: String, qty: u32,}
fn main() { let list = vec![ Item { name: String::from("사과"), qty: 3 }, Item { name: String::from("빵"), qty: 1 }, ];
let json = serde_json::to_string(&list).unwrap(); println!("{}", json);}실행 결과:
[{"name":"사과","qty":3},{"name":"빵","qty":1}]serde는 Vec, String, 숫자를 다루는 방법을 이미 알고 있어서, Item에 Serialize를 derive하는 것만으로 충분해요. to_string_pretty로 바꿔서 같은 목록이 여러 줄로 펼쳐지는 것도 확인해 보세요.
2. 기본값이 있는 설정
섹션 제목: “2. 기본값이 있는 설정”이 JSON을 Settings 구조체로 읽으세요. {"siteTitle": "내 블로그", "postsPerPage": 5}. 구조체에는 site_title, posts_per_page, 그리고 없을 수도 있는 theme가 있어야 해요. 그다음 {"siteTitle": "메모"}도 읽으세요. 이때 posts_per_page는 0이 돼야 해요. 둘 다 {:?}로 출력하세요.
힌트: 구조체에 #[serde(rename_all = "camelCase")], theme에는 Option<String>, posts_per_page에는 #[serde(default)]가 필요해요.
정답 보기
use serde::Deserialize;
#[derive(Deserialize, Debug)]#[serde(rename_all = "camelCase")]struct Settings { site_title: String, #[serde(default)] posts_per_page: u32, theme: Option<String>,}
fn main() { let full = r#"{"siteTitle": "내 블로그", "postsPerPage": 5}"#; let short = r#"{"siteTitle": "메모"}"#;
let a: Settings = serde_json::from_str(full).unwrap(); let b: Settings = serde_json::from_str(short).unwrap();
println!("{:?}", a); println!("{:?}", b);}실행 결과:
Settings { site_title: "내 블로그", posts_per_page: 5, theme: None }Settings { site_title: "메모", posts_per_page: 0, theme: None }rename_all이 siteTitle을 site_title에, postsPerPage를 posts_per_page에 연결해 줘요. 두 JSON 모두 theme가 없으니 둘 다 None이고, 두 번째 JSON에 빠진 postsPerPage는 0이 돼요. #[serde(default)]를 지우면 두 번째 unwrap()이 “missing field postsPerPage”(postsPerPage 필드가 없음)라며 패닉을 일으켜요. 여기서는 JSON을 우리가 직접 썼으니 .unwrap()을 써도 괜찮아요. 실제 입력에는 ?나 match를 쓰세요.
serde(--features derive와 함께)와serde_json을 추가하고, 구조체에#[derive(Serialize, Deserialize)]를 붙이세요.serde_json::to_string/to_string_pretty는 값을 JSON으로,serde_json::from_str은 JSON을 값으로 바꿔요.from_str은Result를 돌려줘요. 실제 입력이라면.unwrap()말고?나match로 처리하세요.- 없을 수도 있는 키에는
Option이나#[serde(default)]를, JSON의 이름에 맞추려면rename/rename_all을 쓰세요. - 모양을 모를 때는
serde_json::Value와json!을 쓰고, 알 때는 구조체를 쓰는 편이 좋아요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. JSON 텍스트를 내 구조체로 바꿀 수 있게 해 주는 derive는 무엇일까요?
2. JSON 객체에 bio 키가 없을 수도 있어요. 그 필드에 알맞은 타입은 무엇일까요?
3. 구조체 필드는 display_name인데 JSON은 displayName을 써요. 구조체 전체에 적용할 가장 간단한 해결책은 무엇일까요?
4. 구조체에 없는 JSON 키를 serde는 기본적으로 어떻게 처리할까요?
점수: 0 / 4
더 알아보기
섹션 제목: “더 알아보기”- serde.rs — 공식 Serde 가이드 (영어): serde가 어떻게 동작하는지 개요와 함께, 여러 데이터 형식의 예제가 있어요.
- Container attributes — serde.rs (영어):
rename_all,deny_unknown_fields처럼 구조체나 열거형 전체에 쓰는 옵션이에요. - Field attributes — serde.rs (영어):
rename,default,skip,skip_serializing_if처럼 필드 하나에 쓰는 옵션이에요. serde_json— docs.rs (영어): 타입 있는 JSON과 타입 없는 JSON,json!매크로의 예제가 있는 크레이트 첫 페이지예요.serde_json::Value— docs.rs (영어):as_str,as_i64,get처럼 타입 없는 JSON을 읽는 모든 메서드가 있어요.