콘텐츠로 이동

Rust Serde로 JSON 다루기

JSON은 어디에나 있어요. 설정 파일, 웹 API, 게임 저장 데이터까지요. Rust에서는 거의 모든 사람이 JSON을 다룰 때 크레이트 두 개를 써요. serde(serialize / deserialize의 줄임말)는 Rust 값을 데이터로 바꾸고 다시 되돌리는 방법을 알아요. serde_json 크레이트는 거기에 JSON 형식을 가르쳐 줘요.

serde를 통역사라고 생각해 보세요. 데이터를 평범한 Rust 구조체로 한 번만 설명해 두면, serde가 JSON으로 바꾸고 다시 되돌리는 일을 대신 해 줘요. JSON 텍스트를 손으로 쓸 일도, JSON을 한 글자씩 뜯어볼 일도 없어요.

프로젝트 안에서 두 크레이트를 추가하세요. derive 기능을 켜면 serde가 변환 코드를 대신 만들어 줘요.

Terminal window
cargo add serde --features derive
cargo add serde_json

이제 Cargo.toml에 이런 내용이 생겨요.

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Cargo.toml과 cargo add가 처음이라면 Cargo와 크레이트를 먼저 읽어 보세요. Rust Playground에는 두 크레이트가 이미 들어 있어서, 이 페이지의 “직접 실행해 보기” 버튼은 아무 준비 없이 모두 동작해요.

구조체에 #[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는 줄바꿈과 공백을 넣어 줘서, 사람이 읽을 파일에 더 잘 어울려요. 키는 구조체의 필드 순서를 그대로 따라가요.

반대 방향을 역직렬화(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은 기대한 모양이 아닐 때가 많아요. 필드가 빠져 있거나, 숫자가 텍스트로 들어오기도 해요. 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으로 만들어요. 키가 없거나 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가 필드를 쉰 개나 보내도, 나에게 필요한 필드만 설명하면 돼요.

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의 모양을 미리 모르거나, 일부분만 필요할 때도 있어요. 그럴 때는 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
그냥 텍스트: 서울
바람: null
▶ 직접 실행해 보기

data["city"]를 출력하면 따옴표가 붙는다는 점을 눈여겨보세요. 아직 JSON 문자열이기 때문이에요. 평범한 Rust 값을 얻으려면 .as_str(), .as_i64(), .as_bool()을 쓰세요. 값이 다른 타입일 수도 있으니, 모두 Option을 돌려줘요.

Value는 유연하지만 컴파일러의 도움을 받을 수 없어요. 모양을 알고 있다면 구조체를 쓰는 편이 좋아요.

실제로 쓸 만한 예제를 볼게요. 앱 설정을 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()을 호출하세요.

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로 바꿔서 같은 목록이 여러 줄로 펼쳐지는 것도 확인해 보세요.

이 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