Serde를 사용한 Rust의 세분화된 JSON 직렬화 제어
Min-jun Kim
Dev Intern · Leapcell

소개
현대 웹 서비스 및 데이터 교환의 세계에서 JSON은 어디에나 존재하는 형식으로 자리 잡고 있습니다. 성능과 안전성에 중점을 둔 Rust는 종종 효율적인 직렬화 및 역직렬화를 위해 serde 크레이트를 사용합니다. serde는 일반적으로 마법을 원활하게 수행하지만, 기본 직렬화 동작이 이상적이지 않은 경우가 자주 있습니다. API가 snake_case의 필드 이름을 예상하지만 Rust 구조체는 camelCase를 선호할 수 있으며, 또는 필드가 None인 경우 해당 필드를 JSON 페이로드에서 생략해야 할 수 있습니다. 이러한 사소해 보이는 불일치는 시스템 통합 및 데이터 처리에서 상당한 마찰을 일으킬 수 있습니다. 다행히 serde는 #[serde(rename_all)] 및 #[serde(skip_serializing_if)]와 같은 강력한 속성을 제공하여 직렬화 프로세스를 세분화하여 제어할 수 있도록 하여 JSON 출력을 요구 사항에 정확하게 맞출 수 있습니다. 이 기사에서는 이러한 속성을 자세히 살펴보고 개발자가 매우 사용자 정의되고 상호 운용 가능한 JSON 표현을 만들 수 있도록 지원하는 방법을 보여줍니다.
Serde의 직렬화 제어 이해
실제 예제를 자세히 살펴보기 전에 논의와 관련된 핵심 serde 개념을 간략하게 정의해 보겠습니다.
- 직렬화: 데이터 구조(Rust
struct또는enum과 같은)를 전송 또는 저장에 적합한 형식(JSON 문자열과 같은)으로 변환하는 프로세스입니다. - 역직렬화: 외부 형식의 데이터를 Rust 데이터 구조로 다시 변환하는 역방향 프로세스입니다.
#[derive(Serialize)]: Rust 유형의 직렬화를 위한 필요한 코드를 자동으로 생성하는serde에서 제공하는 절차적 매크로입니다.- 속성: 컴파일러 또는 다른 도구에 메타데이터를 제공하는 데 사용되는 Rust의 특수 주석입니다.
serde는 사용자 정의를 위해 속성을 활용합니다.
이제 직렬화에 대한 초능력을 제공하는 주요 속성을 살펴보겠습니다.
#[serde(rename_all)]를 사용한 필드 이름 바꾸기
외부 API 또는 기존 시스템과 통합할 때 일반적인 문제는 이름 지정 규칙을 조정하는 것입니다. Rust는 일반적으로 필드 이름에 snake_case를 선호하지만, 특히 JavaScript 또는 Java 생태계에서 파생된 많은 JSON API는 camelCase, PascalCase 또는 kebab-case를 사용할 수 있습니다. #[serde(rename = "new_name")]를 사용하여 각 필드를 수동으로 이름 바꾸는 것은 큰 구조체의 경우 지루하고 오류가 발생하기 쉽습니다.
구조체 수준에서 적용되는 #[serde(rename_all)] 속성은 ( #[derive(Deserialize)]가 있는 경우 역직렬화 시에도) 해당 구조체의 모든 필드에 이름 지정 규칙 변환을 자동으로 적용하여 편리한 솔루션을 제공합니다.
예는 다음과 같습니다.
use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct UserProfile { user_id: String, first_name: String, last_name: String, email_address: Option<String>, } fn main() { let user = UserProfile { user_id: "u123".to_string(), first_name: "Alice".to_string(), last_name: "Smith".to_string(), email_address: Some("alice.smith@example.com".to_string()), }; let json = serde_json::to_string_pretty(&user).unwrap(); println!("Serialized JSON (camelCase):\n{}", json); // Output: // { // "userId": "u123", // "firstName": "Alice", // "lastName": "Smith", // "emailAddress": "alice.smith@example.com" // } let json_input = r#"{ "userId": "u456", "firstName": "Bob", "lastName": "Johnson", "emailAddress": null }"#; let deserialized_user: UserProfile = serde_json::from_str(json_input).unwrap(); println!("Deserialized User: {:?}", deserialized_user); // Output: Deserialized User: UserProfile { user_id: "u456", first_name: "Bob", last_name: "Johnson", email_address: None } }
이 예에서 #[serde(rename_all = "camelCase")]는 user_id를 userId, first_name을 firstName 등으로 자동으로 변환합니다. snake_case, PascalCase, kebab-case, SCREAMING_SNAKE_CASE와 같은 다른 일반적인 변환도 가능합니다. 이는 상용구 코드를 크게 줄이고 코드 가독성을 향상시킵니다.
#[serde(skip_serializing_if)]를 사용한 필드 조건부 생략
또 다른 빈번한 요구 사항은 특정 조건에서 직렬화 된 출력에서 필드를 생략하는 것입니다. 예를 들어, 선택 필드가 None인 경우 null을 보내는 대신 전체를 JSON에서 제외하고 싶을 수 있습니다. 또는 기본값이 있는 필드를 생략하고 싶을 수 있습니다.
개별 필드에 적용되는 #[serde(skip_serializing_if)] 속성은 조건자 함수를 지정할 수 있도록 합니다. 이 함수가 필드 값에 대해 true를 반환하면 해당 필드는 직렬화된 출력에서 완전히 생략됩니다.
UserProfile 예제를 확장하여 이를 시연해 보겠습니다.
use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct UserProfileV2 { user_id: String, first_name: String, last_name: String, #[serde(skip_serializing_if = "Option::is_none")] email_address: Option<String>, #[serde(skip_serializing_if = "Vec::is_empty")] favorite_colors: Vec<String>, #[serde(skip_serializing_if = "is_default_age")] age: u8, } fn is_default_age(age: &u8) -> bool { *age == 0 // 0이 '기본' 또는 설정되지 않은 나이라고 가정 } fn main() { let user1 = UserProfileV2 { user_id: "u123".to_string(), first_name: "Alice".to_string(), last_name: "Smith".to_string(), email_address: Some("alice.smith@example.com".to_string()), favorite_colors: vec!["blue".to_string(), "green".to_string()], age: 30, }; let user2 = UserProfileV2 { user_id: "u456".to_string(), first_name: "Bob".to_string(), last_name: "Johnson".to_string(), email_address: None, // 이 필드는 생략됩니다 favorite_colors: vec![], // 이 필드는 생략됩니다 age: 0, // is_default_age로 인해 이 필드는 생략됩니다 }; println!("User 1 JSON:\n{}", serde_json::to_string_pretty(&user1).unwrap()); // Output: // { // "userId": "u123", // "firstName": "Alice", // "lastName": "Smith", // "emailAddress": "alice.smith@example.com", // "favoriteColors": [ // "blue", // "green" // ], // "age": 30 // } println!("\nUser 2 JSON:\n{}", serde_json::to_string_pretty(&user2).unwrap()); // Output: // { // "userId": "u456", // "firstName": "Bob", // "lastName": "Johnson" // } }
UserProfileV2에서:
#[serde(skip_serializing_if = "Option::is_none")]는Option유형에 대한 표준 라이브러리의is_none메서드를 활용하는 매우 일반적인 패턴입니다.email_address가None인 경우 JSON에 표시되지 않습니다.#[serde(skip_serializing_if = "Vec::is_empty")]는is_empty메서드를Vec에 유사하게 적용하여 빈 목록을 생략합니다.#[serde(skip_serializing_if = "is_default_age")]는 사용자 정의 함수is_default_age를 사용하는 것을 보여줍니다. 이 함수는 필드 값의 참조를 받아 필드를 건너뛰어야 하는 경우true를 반환합니다.
이 속성은 특히 업데이트된 필드만 전송되는 PATCH 요청이나 선택 매개변수의 경우 더 깔끔하고 가벼운 JSON 페이로드를 생성하는 데 매우 유용합니다.
포괄적인 제어를 위한 속성 결합
serde 속성의 진정한 힘은 이를 결합할 때 발휘됩니다. 여러 속성을 동일한 구조체 또는 필드에 사용하여 복잡한 직렬화 동작을 달성할 수 있습니다.
Product 구조체가 있는 시나리오를 고려해 봅시다.
- 모든 필드를 JSON에서
PascalCase로 지정합니다. - 선택적
description필드를None인 경우 생략합니다. tags목록을 비어 있으면 생략합니다.stock_count필드는 명시적으로 설정되지 않은 경우 기본값으로 0을 가지며, 0인 경우 생략됩니다. 이는#[serde(default)]와skip_serializing_if를 결합합니다. 명확성을 위해 별칭을 지정합니다.
use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "PascalCase")] struct Product { id: String, name: String, #[serde(skip_serializing_if = "Option::is_none")] description: Option<String>, #[serde(skip_serializing_if = "Vec::is_empty")] tags: Vec<String>, #[serde(default)] // 역직렬화 시 stock_count의 기본값 의미 #[serde(skip_serializing_if = "is_zero")] stock_count: u32, } fn is_zero(num: &u32) -> bool { *num == 0 } impl Default for Product { fn default() -> Self { Product { id: String::new(), name: String::new(), description: None, tags: Vec::new(), stock_count: 0, } } } fn main() { let product1 = Product { id: "prod-101".to_string(), name: "Super Widget".to_string(), description: Some("A high-quality widget for all your needs.".to_string()), tags: vec!["gadget".to_string(), "electronics".to_string()], stock_count: 50, }; let product2 = Product { id: "prod-202".to_string(), name: "Basic Gadget".to_string(), description: None, tags: Vec::new(), stock_count: 0, // 이 값은 명시적으로 주어지지 않으면 암시적으로 생성된 후 생략됩니다 }; println!("Product 1 JSON:\n{}", serde_json::to_string_pretty(&product1).unwrap()); // Output: // { // "Id": "prod-101", // "Name": "Super Widget", // "Description": "A high-quality widget for all your needs.", // "Tags": [ // "gadget", // "electronics" // ], // "StockCount": 50 // } println!("\nProduct 2 JSON:\n{}", serde_json::to_string_pretty(&product2).unwrap()); // Output: // { // "Id": "prod-202", // "Name": "Basic Gadget" // } }
Product에서:
#[serde(rename_all = "PascalCase")]는 모든 필드가PascalCase인지 확인합니다.description은None인 경우 생략됩니다.tags는 비어 있는 경우 생략됩니다.stock_count는#[serde(default)]와#[serde(skip_serializing_if)]의 사용자 정의is_zero함수를 사용합니다. 이는 JSON을 역직렬화할 때StockCount를 포함하지 않으면 기본값으로0이 된다는 것을 의미합니다. 그런 다음 직렬화할 때stock_count가0이면 생략됩니다. 이렇게 하면 기본값이 아닌 재고 수량만 포함된 깔끔한 JSON을 만들 수 있습니다.
이러한 속성은 유연하고 강력하며 관용적인 Rust 애플리케이션을 만들고 다양한 JSON API와 원활하게 상호 작용하는 데 필수적입니다. 수동 데이터 변환 로직의 필요성을 최소화하여 직렬화 문제를 데이터 구조에 가깝게 선언적으로 유지합니다.
결론
#[serde(rename_all)] 및 #[serde(skip_serializing_if)]와 같은 속성을 통해 serde를 사용하여 Rust에서 JSON 직렬화를 사용자 정의하는 것은 높은 상호 운용성을 달성하고 깔끔하고 효율적인 페이로드를 생성하는 강력한 기술입니다. Rust 구조체에서 직접 명명 규칙과 조건부 필드 생략을 선언적으로 정의함으로써 상용구 코드를 크게 줄이고 애플리케이션의 데이터 표현이 외부 요구 사항과 완벽하게 일치하도록 할 수 있습니다. 이러한 속성을 숙달하면 개발자는 JSON 중심 세계에서 성능이 뛰어나고 매우 적응력이 뛰어난 Rust 애플리케이션을 만들 수 있습니다.

