Danh sách bài viết

Bài 48: JSON Parse Error Detail — Line, Column, Path

Bài 48 của series Rust RESTful API — error message default của serde_json khi parse fail có sẵn 2 thông tin line + column (vị trí byte trong text body) qua method error.line() + error.column() nhưng KHÔNG có path field đầy đủ — client UI nhận được message "expected value at line 1 column 24" không biết field NÀO trong struct fail, phải đếm tay byte trong body để map ngược, UX kém; serde_json::error::Category enum 4 variant phân loại nguyên nhân fail: Io lỗi đọc input network/file (gửi 500 Internal Server Error vì server lỗi đọc body, KHÔNG phải client xấu), Syntax JSON cú pháp sai missing comma/bracket/dấu nháy (gửi 400 Bad Request — client xấu), Data JSON hợp lệ syntax nhưng không match struct Rust mismatch type/missing field required (gửi 400 hoặc 422 tùy semantic Shop API chọn 400 vì parse fail không phải business rule fail), Eof input cụt giữa chừng connection drop hoặc body truncate (gửi 400); crate serde_path_to_error wrap deserializer track path khi traverse struct nested deep — output format hỗ trợ field_name top-level + parent.child nested + items[2] array index + combined order.items[0].metadata["key"] map key, cho phép client UI highlight đúng field fail ở form deep nested; refactor AppJson extractor lock B32 — thay serde_json::from_slice direct bằng serde_path_to_error::deserialize(deserializer) wrap inner serde_json deserializer, match inner.classify() trả 2 lỗi structured: Io | Syntax | Eof → AppError::JsonSyntax{message, line, column} với 3 field structured cho client UI render, Data → AppError::JsonDataMismatch{path, message} với path field deep cho UI highlight field; extend AppError enum lock B16 thêm 2 variant mới JsonSyntax (3 field) + JsonDataMismatch (2 field) với impl IntoResponse 2 match arm build envelope JSON khác variant existing — top-level flat (error, code, request_id) lock B16 + nested detail object chứa structured info riêng từng error type (detail.line + detail.column + detail.message cho syntax, detail.path + detail.message cho data mismatch), bump variant count từ 12 → 14; Stripe-style error envelope nested wrap mọi field trong 1 object error top-level ({error: {type, code, param, message}}) so sánh với Shop API flat envelope lock B16 từ đầu series — quyết định GIỮ flat structure cho consistency với ValidationFailed 422 đã lock fields object B41 (cùng pattern top-level + nested object riêng từng case), customize chi tiết qua detail object structured cho mỗi error variant cần thông tin thêm (JsonSyntax có line+column, JsonDataMismatch có path, future variant khác có field riêng); pattern envelope đầy đủ lock B48: top-level 3 field MANDATORY (error human message, code machine SCREAMING_SNAKE, request_id correlation UUID enrich qua middleware B39), nested object optional theo variant (detail cho JsonSyntax/JsonDataMismatch structured info, fields object B41 cho ValidationFailed field-name → messages); trade-off security expose path field qua error response — pros client dev biết chính xác field name nào fail trong nested deep struct (order.items[2].quantity) để highlight UI form input, fix bug request nhanh không phải đoán field; cons lộ internal struct field name → attacker enumeration đoán schema model (cấu trúc DTO Rust internal lộ ra wire), guess relationship giữa entity (User có field roles, Order có field items[].product_id) → đoán attack vector → tăng attack surface; mitigation env flag production disable detail object qua AppConfig.expose_error_detail: bool default true dev/staging false production — chỉ trả error + code + request_id top-level prod hide schema giữ debug-ability qua server-side log full chi tiết qua tracing với request_id correlation (G19 deep configure env-driven sau); file path lock: refactor crates/shop-api/src/extractors/json.rs AppJson from_request dùng serde_path_to_error wrap deserializer + match Category 4 variant + extend crates/shop-common/src/error.rs AppError enum thêm 2 variant JsonSyntax + JsonDataMismatch với IntoResponse 2 match arm build envelope detail object; workspace deps mới: serde_path_to_error = "0.1" thêm [workspace.dependencies] Cargo.toml root + consume qua shop-api/Cargo.toml serde_path_to_error.workspace = true; foundation cho B49 (NDJSON newline-delimited JSON parse error multi-line — mỗi line 1 JSON object, line-by-line error report với line index cho biết line nào fail không cần load hết file vào RAM), B50 (compression decode error chain — gzip/brotli body decompress fail trước khi serde_json parse, classify thêm error variant DecodeError với encoding type + original encoded size + decoded position).

14/06/2026
12 phút đọc
0 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Hiểu error message default của serde_json — có line + column nhưng KHÔNG có path field.
  • Phân biệt 4 loại error parse JSON qua serde_json::error::Category: Io, Syntax, Data, Eof.
  • Áp dụng crate serde_path_to_error để có path field đầy đủ như order.items[2].quantity.
  • Customize error envelope theo Stripe-style (param, message, code) — so sánh với flat envelope Shop API.
  • Refactor AppJson extractor (lock B32) Shop API trả 2 lỗi structured: JsonSyntax với line+column + JsonDataMismatch với path.
  • Hiểu trade-off security khi expose internal struct path qua error response — pros UX dev vs cons lộ schema attacker.
2

serde_json Default Error — Line + Column

Khi parse JSON fail, serde_json::Error có sẵn 2 thông tin định vị byte trong text body: line (số dòng, 1-based) và column (cột, 1-based). Truy cập qua method error.line() + error.column():

use serde::Deserialize;

#[derive(Deserialize)]
struct CreateProductDto {
    name: String,
    price: u64,
}

fn main() {
    // JSON thiếu value sau "price":
    let json = r#"{"name": "x", "price": }"#;

    let err = serde_json::from_str::<CreateProductDto>(json).unwrap_err();
    println!("{}", err);
    // expected value at line 1 column 24

    println!("line={} column={}", err.line(), err.column());
    // line=1 column=24
}

3 method chính của serde_json::Error:

  • error.line() -> usize — dòng vị trí lỗi (1-based, 0 nếu unknown).
  • error.column() -> usize — cột vị trí lỗi (1-based, 0 nếu unknown).
  • error.classify() -> Category — phân loại nguyên nhân lỗi qua enum 4 variant (bước 3 sẽ deep).

Pitfall không có path field: với JSON nested deep như {"order": {"items": [{"id": 1, "quantity": "abc"}]}}, error trả về chỉ có "expected u32 at line 1 column 47" — client UI biết byte 47 fail nhưng KHÔNG biết field nào trong struct Rust fail. Dev frontend phải đếm tay byte trong body, map ngược về vị trí field order.items[0].quantity mới highlight được UI input.

Use case cần path field: form admin tạo product với nested categories + variants + metadata, request fail thì UI cần highlight ĐÚNG input field người dùng vừa nhập sai. Báo cáo error chung chung "JSON invalid" không đủ — user phải tự dò 30 field form. Bước 4 sẽ giới thiệu serde_path_to_error crate giải quyết.

3

4 Loại serde_json::error::Category

Enum serde_json::error::Category phân loại nguyên nhân parse fail thành 4 variant, mỗi loại cần status code HTTP khác nhau:

  • Io — lỗi đọc input (network drop, file read fail). Server lỗi đọc body, KHÔNG phải client xấu → gửi 500 Internal Server Error.
  • Syntax — JSON cú pháp sai (missing comma, bracket, dấu nháy). Client xấu → 400 Bad Request.
  • Data — JSON hợp lệ syntax nhưng không match struct Rust (mismatch type, missing field required). Có thể gửi 400 (parse fail) hoặc 422 (business rule fail) tùy semantic API.
  • Eof — input cụt giữa chừng ({"x": rồi end stream). Client gửi body không complete → 400 Bad Request.

Phân loại qua match err.classify():

use serde_json::error::Category;
use shop_common::error::AppError;

fn map_serde_json_error(err: serde_json::Error) -> AppError {
    match err.classify() {
        Category::Io => AppError::Internal(anyhow::anyhow!(
            "IO error reading body: {}", err
        )),
        Category::Syntax => AppError::BadRequest(format!(
            "JSON syntax error: {}", err
        )),
        Category::Data => AppError::BadRequest(format!(
            "JSON data mismatch: {}", err
        )),
        Category::Eof => AppError::BadRequest(
            "JSON cut off mid-parse".into()
        ),
    }
}

Quyết định Shop API map Data → 400 thay 422: lock B3 quy định 422 là "business rule fail" (validate fail sau parse — vd email format sai, slug trùng). Data mismatch type là "parse fail" (client gửi "abc" cho field type u32) → semantic giống Syntax hơn → 400 BAD_REQUEST. Lock B41 ValidationFailed mới là 422 envelope với fields object.

Bước 5 sẽ refactor AppJson extractor map gộp Io | Syntax | EofJsonSyntax variant mới (chứa line+column), riêng DataJsonDataMismatch variant mới (chứa path) — cho client UI 2 kiểu detail khác nhau.

4

serde_path_to_error — Path Đầy Đủ

Crate serde_path_to_error (dtolnay maintain, version 0.1.x stable) wrap deserializer track path khi traverse struct nested. Khi gặp lỗi tại field deep, trả về Error chứa path đầy đủ + inner serde_json error gốc.

Thêm dependency vào workspace root Cargo.toml:

# shop/Cargo.toml — [workspace.dependencies]
[workspace.dependencies]
# ... existing deps
serde_path_to_error = "0.1"

Consume ở crates/shop-api/Cargo.toml:

# crates/shop-api/Cargo.toml
[dependencies]
# ... existing
serde_path_to_error = { workspace = true }

Cách dùng cơ bản — wrap serde_json::Deserializer:

use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Order {
    items: Vec<Item>,
}

#[derive(Deserialize, Debug)]
struct Item {
    id: u64,
    quantity: u32,
}

fn main() {
    // items[0].quantity sai type — gửi string "abc" cho u32
    let json = r#"{"items": [{"id": 1, "quantity": "abc"}]}"#;

    let de = &mut serde_json::Deserializer::from_str(json);
    let result: Result<Order, _> = serde_path_to_error::deserialize(de);

    match result {
        Ok(order) => println!("ok: {:?}", order),
        Err(err) => {
            println!("path:  {}", err.path());
            // path:  items[0].quantity

            println!("inner: {}", err.inner());
            // inner: invalid type: string "abc", expected u32 at line 1 column 38
        }
    }
}

Output format err.path() qua Display trait hỗ trợ 4 trường hợp:

Trường hợp        | Format
Field top-level   | name
Nested field      | order.customer.email
Array index       | items[2]
Map key           | metadata["warranty_months"]
Combined deep     | order.items[0].metadata["color"]

Path đủ thông tin cho client UI highlight đúng field. err.inner() trả về &serde_json::Error gốc — có thể gọi .classify(), .line(), .column() như bước 2-3.

5

Refactor AppJson Trả Path Detail

Refactor extractor AppJson<T> (lock B32) — thay logic delegate axum::Json bằng pattern đọc Bytes body raw rồi parse qua serde_path_to_error. Map error theo Category: Io | Syntax | Eof gộp thành JsonSyntax (line+column), Data riêng thành JsonDataMismatch (path).

// File: crates/shop-api/src/extractors/json.rs (refactor B48)
use axum::{
    body::Bytes,
    extract::{FromRequest, Request},
    http::{header, HeaderMap},
};
use serde::de::DeserializeOwned;
use serde_json::error::Category;
use shop_common::error::AppError;

pub struct AppJson<T>(pub T);

impl<T, S> FromRequest<S> for AppJson<T>
where
    T: DeserializeOwned,
    S: Send + Sync,
{
    type Rejection = AppError;

    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
        // 1. Validate Content-Type
        if !is_json_content_type(req.headers()) {
            return Err(AppError::BadRequest(
                "Content-Type phải là application/json".into(),
            ));
        }

        // 2. Extract bytes (size limit đã handle bởi DefaultBodyLimit layer B32)
        let bytes = Bytes::from_request(req, state)
            .await
            .map_err(|_| AppError::BadRequest("không đọc được body".into()))?;

        // 3. Deserialize qua serde_path_to_error
        let de = &mut serde_json::Deserializer::from_slice(&bytes);
        let value: T = serde_path_to_error::deserialize(de).map_err(|e| {
            let path = e.path().to_string();
            let inner = e.inner();

            match inner.classify() {
                Category::Io | Category::Syntax | Category::Eof => {
                    AppError::JsonSyntax {
                        message: inner.to_string(),
                        line: inner.line(),
                        column: inner.column(),
                    }
                }
                Category::Data => AppError::JsonDataMismatch {
                    path,
                    message: inner.to_string(),
                },
            }
        })?;

        Ok(AppJson(value))
    }
}

fn is_json_content_type(headers: &HeaderMap) -> bool {
    headers
        .get(header::CONTENT_TYPE)
        .and_then(|v| v.to_str().ok())
        .map(|s| s.contains("application/json"))
        .unwrap_or(false)
}

3 thay đổi chính so với B32:

  • KHÔNG delegate axum::Json::from_request nữa — đọc Bytes raw rồi parse trực tiếp qua serde_path_to_error.
  • Trả 2 variant AppError structured mới (JsonSyntax + JsonDataMismatch) thay 1 string verbose như B32.
  • Path field deep nested (order.items[0].quantity) có sẵn cho client UI highlight đúng field.

Pattern lock B48: mọi custom JSON extractor Shop API tương lai (NDJSON B49, Multipart JSON part B36) MANDATORY dùng serde_path_to_error wrap deserializer — KHÔNG dùng serde_json::from_slice direct vì mất path info.

6

Thêm 2 AppError Variant Mới

Extend enum AppError trong crates/shop-common/src/error.rs thêm 2 variant — bump tổng số variant từ 12 (B41 lock) lên 14:

// File: crates/shop-common/src/error.rs (extend B48)
#[derive(Debug, thiserror::Error)]
pub enum AppError {
    // ... 12 variants existing (B41)

    #[error("JSON syntax error at line {line} column {column}: {message}")]
    JsonSyntax {
        message: String,
        line: usize,
        column: usize,
    },

    #[error("JSON data mismatch at {path}: {message}")]
    JsonDataMismatch {
        path: String,
        message: String,
    },
}

Update impl IntoResponse for AppError thêm 2 match arm build envelope top-level flat lock B16 + nested detail object chứa structured info riêng:

// File: crates/shop-common/src/error.rs (extend impl IntoResponse)
use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde_json::json;

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        match &self {
            // ... 12 arm existing

            AppError::JsonSyntax { message, line, column } => {
                let body = json!({
                    "error": "JSON syntax error",
                    "code": "JSON_SYNTAX",
                    "request_id": null,
                    "detail": {
                        "line": line,
                        "column": column,
                        "message": message,
                    }
                });
                (StatusCode::BAD_REQUEST, Json(body)).into_response()
            }

            AppError::JsonDataMismatch { path, message } => {
                let body = json!({
                    "error": "JSON data mismatch",
                    "code": "JSON_DATA_MISMATCH",
                    "request_id": null,
                    "detail": {
                        "path": path,
                        "message": message,
                    }
                });
                (StatusCode::BAD_REQUEST, Json(body)).into_response()
            }
        }
    }
}

2 envelope JSON wire format:

// JsonSyntax variant — 400 Bad Request
{
  "error": "JSON syntax error",
  "code": "JSON_SYNTAX",
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "detail": {
    "line": 1,
    "column": 24,
    "message": "expected value at line 1 column 24"
  }
}
// JsonDataMismatch variant — 400 Bad Request
{
  "error": "JSON data mismatch",
  "code": "JSON_DATA_MISMATCH",
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "detail": {
    "path": "items[0].quantity",
    "message": "invalid type: string \"abc\", expected u32 at line 1 column 38"
  }
}

Field request_id placeholder null tạm thời — middleware B39 enrich_error_response đã wire để inject request_id thật qua re-parse JSON body trước khi gửi response. Mọi error envelope Shop API consistency có request_id cho correlation log/tracing.

7

Stripe-Style Error Envelope

Stripe API error envelope wrap mọi field bên trong object error top-level — pattern industry phổ biến:

// Stripe-style nested
{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_invalid",
    "param": "amount",
    "message": "amount must be a positive integer",
    "doc_url": "https://stripe.com/docs/error-codes/parameter-invalid"
  }
}

So sánh với Shop API flat envelope lock B16 từ đầu series:

// Shop API flat top-level
{
  "error": "amount must be a positive integer",
  "code": "VALIDATION_FAILED",
  "request_id": "550e8400-...",
  "fields": {
    "amount": ["amount phải lớn hơn 0"]
  }
}

2 trade-off chính khi chọn:

  • Stripe nested — wrap mọi field trong 1 namespace error, dễ extend field metadata (doc_url, request_log_url, charge_id) không phá level top.
  • Flat top-level (Shop API) — đơn giản hơn cho client parse, consistent với success response ({data, meta} sẽ là {error, code, request_id} cùng level), match pattern envelope đã lock B41 ValidationFailed với fields object.

Lock decision Shop API GIỮ flat envelope vì 3 lý do:

  • Consistency với envelope ValidationFailed 422 lock B41 (fields object top-level cùng error + code + request_id) — refactor sang Stripe nested phải đổi B41 + B16 cùng lúc, breaking change.
  • Client parse 1 level đơn giản hơn — response.code thay response.error.code, ít typo, error message dev đọc log trace dễ hơn.
  • Field metadata extend qua nested detail object per variant — flexibility vẫn đủ, không cần wrap nested top-level.

Pattern envelope đầy đủ lock B48 vĩnh viễn:

Top-level (MANDATORY mọi error):
  error        human message (string) — Display trait từ thiserror
  code         machine SCREAMING_SNAKE (string) — "JSON_SYNTAX", "VALIDATION_FAILED"
  request_id   correlation UUID (string|null) — enrich qua middleware B39

Nested object (optional theo variant):
  detail       structured info riêng từng error type
                 - JsonSyntax: line + column + message
                 - JsonDataMismatch: path + message
                 - future: variant tự define field
  fields       field-name → messages map (cho ValidationFailed 422 lock B41)

Variant nào cần structured info thêm thì add object con tương ứng (detail cho JSON parse, fields cho ValidationFailed, future retry cho RateLimited extend dynamic). Pattern này extend dễ, không phá envelope existing.

8

Verify + Trade-Off Security

Verify end-to-end với 3 case curl:

cargo run -p shop-api
# Output: shop-api listening addr=0.0.0.0:3000

Test 1: JSON syntax fail (thiếu value sau "price"):

curl -i -X POST http://localhost:3000/api/v1/products \
  -H 'Content-Type: application/json' \
  -d '{"name": "x", "price": }'

# HTTP/1.1 400 Bad Request
# content-type: application/json; charset=utf-8
# x-request-id: 550e8400-e29b-41d4-a716-446655440000
#
# {
#   "error": "JSON syntax error",
#   "code": "JSON_SYNTAX",
#   "request_id": "550e8400-e29b-41d4-a716-446655440000",
#   "detail": {
#     "line": 1,
#     "column": 24,
#     "message": "expected value at line 1 column 24"
#   }
# }

Test 2: JSON data mismatch (type sai trong nested struct — gửi string cho field price: Money):

curl -i -X POST http://localhost:3000/api/v1/products \
  -H 'Content-Type: application/json' \
  -d '{"name": "x", "slug": "x-slug", "price": "abc", "stock": 1}'

# HTTP/1.1 400 Bad Request
# content-type: application/json; charset=utf-8
#
# {
#   "error": "JSON data mismatch",
#   "code": "JSON_DATA_MISMATCH",
#   "request_id": "550e8400-...",
#   "detail": {
#     "path": "price",
#     "message": "invalid type: string \"abc\", expected ..."
#   }
# }

Test 3: Nested array deep (bulk import items với field sai type ở index 2):

curl -i -X POST http://localhost:3000/api/v1/admin/products/import \
  -H 'Content-Type: application/json' \
  -d '{"items": [{"name":"a","price":100},{"name":"b","price":200},{"name":"c","price":"xyz"}]}'

# HTTP/1.1 400 Bad Request
#
# {
#   "error": "JSON data mismatch",
#   "code": "JSON_DATA_MISMATCH",
#   "request_id": "...",
#   "detail": {
#     "path": "items[2].price",
#     "message": "invalid type: string \"xyz\", expected u64 ..."
#   }
# }

Path items[2].price chỉ đúng field nested deep → client UI parse path tách items + index 2 + field price → highlight đúng row trong bulk form.

Trade-off security expose path field:

  • Pros: client dev biết chính xác field name nào fail trong struct deep → fix UI highlight nhanh, không phải đoán; reduce thời gian debug request fail từ 30 phút (đếm tay byte) xuống 1 phút (đọc path).
  • Cons: lộ internal struct field name của Rust DTO ra wire → attacker enumeration đoán schema (User có roles, Order có items[].product_id, Cart có discount_code), guess relationship entity → tăng attack surface; nếu schema có field nhạy cảm (vd internal_audit_flag, kyc_status) — lộ qua error.

Mitigation env flag: thêm field expose_error_detail: bool vào AppConfig (B10) — default true dev/staging, false production. Khi false, IntoResponse chỉ trả top-level error + code + request_id, ẩn detail object. Debug-ability giữ qua server-side log full chi tiết trong tracing::error! với request_id correlation — dev nội bộ tra log theo request_id thấy detail, client/attacker bên ngoài chỉ thấy generic message.

Lock decision Shop API: giữ detail trong dev/staging cho UX dev nhanh, disable trong production qua env config AppConfig.expose_error_detail = false default. Group 19 (Observability) sẽ deep implement env-driven config + log correlation pattern.

9

Tổng Kết

  • serde_json default error: có sẵn line + column qua error.line() + error.column(), KHÔNG có path field nên client UI không biết field nào fail.
  • 4 Category: Io (500), Syntax (400), Data (400 hoặc 422 tùy semantic — Shop API chọn 400), Eof (400). Phân loại để gửi status code phù hợp.
  • Crate serde_path_to_error: wrap deserializer track path đầy đủ — field, parent.child, items[2], combined order.items[0].metadata["key"].
  • 2 AppError variant mới: JsonSyntax { message, line, column } (variant 13) + JsonDataMismatch { path, message } (variant 14), bump tổng từ 12 → 14.
  • Stripe-style error envelope nested wrap object error top-level — so sánh với Shop API flat lock B16.
  • Lock decision Shop API GIỮ flat envelope: consistency với ValidationFailed 422 fields object B41 + client parse 1 level đơn giản + field metadata extend qua detail nested object.
  • Pattern envelope đầy đủ lock B48: top-level 3 field MANDATORY (error, code, request_id) + nested detail object cho JsonSyntax/JsonDataMismatch + fields cho ValidationFailed.
  • AppJson refactor: thay delegate axum::Json bằng đọc Bytes raw + parse qua serde_path_to_error::deserialize + match Category 4 variant gộp Io|Syntax|EofJsonSyntax, DataJsonDataMismatch.
  • Trade-off security: expose path field UI-friendly cho dev fix nhanh vs lộ internal schema attacker → mitigation env flag AppConfig.expose_error_detail default dev true production false.
  • request_id enrich qua middleware B39 (consistent với mọi error variant — top-level field).
  • File path lock: crates/shop-api/src/extractors/json.rs refactor; crates/shop-common/src/error.rs 2 variant mới + 2 match arm IntoResponse.
  • Workspace deps mới: serde_path_to_error = "0.1" thêm [workspace.dependencies] + consume qua shop-api/Cargo.toml.
  • Foundation cho B49 (NDJSON parse error multi-line — line-by-line error report), B50 (compression decode error category — gzip/brotli decompress fail trước serde parse).
10

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. serde_json default error có thông tin gì khi parse fail? Tại sao KHÔNG có path field? Use case nào cần path field?
  2. 4 Category là gì? Mỗi loại nên trả status code HTTP nào và lý do? Shop API map Data sang 400 hay 422, vì sao?
  3. serde_path_to_error add gì cho error message? Cho ví dụ path nested với array index và map key.
  4. Stripe-style error envelope có structure gì? So sánh với Shop API flat envelope — 2 trade-off chính và lý do Shop API GIỮ flat.
  5. Trade-off expose path field qua error message — pros và cons? Mitigation nào áp dụng cho production và làm sao dev vẫn debug được?
Đáp án
  1. serde_json default error thông tin có sẵn: 2 thông tin định vị byte trong text body — error.line() -> usize dòng (1-based) và error.column() -> usize cột (1-based) + error.classify() -> Category phân loại nguyên nhân fail. Vd parse r#"{"name": "x", "price": }"# fail thiếu value sau "price" → error.line() == 1, error.column() == 24. Tại sao KHÔNG có path field: serde_json::Error được build trong serde_json::Deserializer internal — deserializer parse từng token JSON sequential KHÔNG track ngữ cảnh "đang ở field nào của struct Rust" vì serde framework design tách deserializer (format-agnostic) và visitor (type-aware). Deserializer chỉ biết "đang ở byte vị trí X" chứ không biết "field X thuộc order.items[0].quantity". Path field cần track qua wrapper crate riêng — serde_path_to_error bước 4. Use case cần path field: (a) form admin Shop API tạo product với 30 field nested + categories + variants + metadata → request fail thì UI cần highlight đúng input field người vừa nhập sai, báo cáo "JSON invalid line 5 column 17" không đủ — user phải tự dò 30 field; (b) bulk import endpoint admin (POST /api/v1/admin/products/import với array 1000 items) — fail tại items[847].price sai type, client cần biết row 847 fail thay đếm tay 850 row JSON; (c) webhook integration third-party (Stripe, GitHub) gửi nested payload phức tạp — debug payload fail phải biết path để check spec docs đúng field; (d) GraphQL/JSON Schema client auto-generate form từ schema — fail map ngược path field UI library highlight tự động.
  2. 4 Category + status code mapping. (a) Io: lỗi đọc input — network drop khi đọc body từ socket, file read fail nếu parse từ disk (vd CSV import streaming). Lỗi phía server (kết nối/IO), KHÔNG phải client xấu → gửi 500 Internal Server Error + log tracing::error! để dev biết investigate. Hiếm gặp ở HTTP handler vì tower-http buffer body sẵn — chủ yếu xuất hiện ở CLI tool đọc file lớn. (b) Syntax: JSON cú pháp sai — missing comma {"a":1 "b":2}, missing bracket {"items": [1, 2, dấu nháy lệch {'name': 'x'} (JSON yêu cầu double-quote không single-quote), trailing comma {"a":1,}. Client gửi body invalid → gửi 400 Bad Request. (c) Data: JSON hợp lệ syntax nhưng KHÔNG match struct Rust — type mismatch (gửi "abc" cho field price: u32), missing field required (struct require name nhưng body không có), unknown variant enum (gửi "unknown" cho field status: OrderStatus chỉ accept "pending"/"paid"/"shipped"). Có thể 400 hoặc 422 tùy semantic API. (d) Eof: input cụt giữa chừng — {"x": rồi end stream (connection drop, body truncate, file read EOF sớm). Client gửi body không complete → gửi 400 Bad Request. Shop API map Data → 400 thay 422 vì 3 lý do: (i) lock B3 quy định 422 là "business rule fail" (validate fail sau parse — email format sai, slug trùng, stock dưới 0 vi phạm business rule). Data mismatch type là "parse fail" — client gửi "abc" cho field u32 không phải vi phạm business rule, mà gửi sai schema; (ii) consistency với Syntax + Eof cũng 400 — cả 3 đều là "client gửi body invalid không thể parse vào struct Rust"; (iii) lock B41 ValidationFailed mới là 422 envelope với fields object (validator crate chạy SAU khi parse thành công). Nếu map Data → 422 sẽ chồng chéo semantic với ValidationFailed — client confuse 2 case dùng cùng status code 422 nhưng envelope khác (1 có detail, 1 có fields).
  3. serde_path_to_error add gì: crate wrap deserializer track path field đầy đủ khi traverse struct nested. Khi gặp lỗi tại field deep, trả về Error chứa: (a) err.path() trả &Path impl Display trait — output format string path đầy đủ; (b) err.inner() trả &serde_json::Error gốc — có thể call .classify(), .line(), .column() như default. Wire usage: thay serde_json::from_slice(&bytes) bằng pattern wrap deserializer:
    let de = &mut serde_json::Deserializer::from_slice(&bytes);
    let value: T = serde_path_to_error::deserialize(de)?;
    
    Format err.path() output 4 trường hợp: (i) field top-level: name (chỉ tên field); (ii) nested field: order.customer.email (dot-separator giữa parent.child); (iii) array index: items[2] (bracket index 0-based); (iv) map key: metadata["warranty_months"] (bracket + double-quote key string); (v) combined deep: order.items[0].variants[1].metadata["color"] kết hợp đủ 4 case. Ví dụ nested array + map key:
    #[derive(Deserialize)]
    struct Order {
        items: Vec<OrderItem>,
    }
    
    #[derive(Deserialize)]
    struct OrderItem {
        id: u64,
        metadata: HashMap<String, serde_json::Value>,
    }
    
    let json = r#"{"items": [
        {"id": 1, "metadata": {"color": "red"}},
        {"id": 2, "metadata": {"warranty_months": "abc"}}
    ]}"#;
    // Parse fail tại items[1].metadata key "warranty_months"
    // nếu type metadata expect u32 thay Value
    // path: items[1].metadata["warranty_months"]
    // inner: invalid type: string "abc", expected u32 at line 3 column 56
    
    Path đủ thông tin cho client UI parse tách: array name items + index 1 + field metadata + map key "warranty_months" → highlight ĐÚNG cell trong table form UI. KHÔNG có serde_path_to_error chỉ biết "line 3 column 56" — phải đếm tay JSON body.
  4. Stripe-style nested envelope structure:
    {
      "error": {
        "type": "invalid_request_error",
        "code": "parameter_invalid",
        "param": "amount",
        "message": "amount must be a positive integer",
        "doc_url": "https://stripe.com/docs/error-codes/parameter-invalid"
      }
    }
    
    Mọi field error wrap trong 1 object error top-level — type (loại error), code (machine code), param (field name fail), message (human-readable), doc_url (link docs Stripe). Shop API flat envelope lock B16:
    {
      "error": "amount must be a positive integer",
      "code": "VALIDATION_FAILED",
      "request_id": "550e8400-...",
      "fields": {
        "amount": ["amount phải lớn hơn 0"]
      }
    }
    
    Top-level 3 field MANDATORY (error, code, request_id) + optional nested object riêng từng variant (fields cho ValidationFailed 422 lock B41, detail cho JsonSyntax/JsonDataMismatch lock B48). 2 trade-off chính: (i) Stripe nested pros dễ extend metadata (doc_url, request_log_url, charge_id Stripe-specific) không phá level top, namespace error chứa tất cả info; cons client phải parse 2 level (response.error.code) thay 1 level, code dài hơn dễ typo, response success/error structure khác nhau (success {data, meta} top-level, error {error: {...}} nested) — client phải biết check status code trước khi parse. (ii) Flat top-level pros đơn giản parse 1 level (response.code), consistent với success response ({data, meta} + {error, code, request_id} cùng pattern top-level), match B41 ValidationFailed với fields top-level đã lock; cons ít namespace cho metadata phức tạp — phải dùng nested object con per variant (detail, fields, future retry) extend dynamic, nếu cần wrap toàn bộ thì phải refactor breaking change. Shop API lock GIỮ flat vì 3 lý do: (a) consistency với ValidationFailed 422 fields object B41 — refactor sang Stripe nested phải đổi B41 + B16 + B48 cùng lúc, breaking client production; (b) client parse 1 level đơn giản hơn — đặc biệt VN team mới mid-level dev đọc log debug dễ; (c) field metadata extend đủ qua nested object con per variant (detail cho JSON parse, fields cho validation, future variant tự define field) — flexibility vẫn đủ, không cần wrap nested top-level.
  5. Trade-off expose path field qua error message. Pros: (i) client dev biết chính xác field name nào fail trong struct deep nested — fix UI highlight nhanh, không phải đoán field; (ii) reduce thời gian debug request fail từ 30 phút (đếm tay byte JSON body) xuống 1 phút (đọc path items[2].price); (iii) UX user cuối cùng tốt hơn — form admin highlight đúng input người vừa nhập sai, không phải đỏ toàn form chung chung; (iv) bulk import endpoint admin (POST /admin/products/import) báo cáo row nào fail trong batch 1000 items — admin sửa 1 row thay re-upload toàn file; (v) integration test viết dễ hơn — assert error.detail.path == "items[0].quantity" precise thay grep substring vague. Cons: (i) lộ internal struct field name Rust DTO ra wire → attacker enumeration đoán schema (User có roles + permissions, Order có items[].product_id + shipping.address.country, Cart có discount_code + applied_promo) — guess relationship entity → tăng attack surface mapping; (ii) nếu schema có field nhạy cảm (vd internal_audit_flag, kyc_status, fraud_score chỉ admin internal dùng) — lộ qua error khi attacker fuzz body intentionally trigger parse error; (iii) version schema lộ qua field naming convention (v2_pricing_strategy, legacy_payment_method) — attacker biết version migration progress; (iv) competitor reverse-engineer business model qua field structure (vd Stripe schema lộ radar_session_id → competitor đoán fraud detection model). Mitigation env flag áp dụng cho production: thêm field expose_error_detail: bool vào AppConfig (B10) — default true dev/staging cho UX dev nhanh, false production. Khi false, IntoResponse match arm JsonSyntax/JsonDataMismatch chỉ trả top-level error + code + request_id, ẩn detail object — client production thấy "JSON syntax error" generic. Dev vẫn debug được qua server-side log: impl IntoResponse for AppError luôn tracing::error!(error = ?self, request_id = %req_id, "json parse fail") log full chi tiết server-side với request_id correlation. Dev nội bộ tra log theo request_id (client gửi header X-Request-Id hoặc lấy từ response header) thấy detail full — không cần wire chi tiết về client. Pattern: "client minimal, server verbose" — wire response ngắn gọn an toàn, log server đầy đủ cho debug. Group 19 (Observability) sẽ deep implement env-driven config AppConfig.expose_error_detail + log correlation pattern với tracing span attach request_id + structured field. Lock decision Shop API: giữ detail trong dev/staging cho UX dev, disable production qua env config default false — dev cần test production-like phải explicit set EXPOSE_ERROR_DETAIL=true trong staging environment.
11

Bài Tiếp Theo

— newline-delimited JSON cho bulk export/import, parse line-by-line không load all to RAM, axum response stream NDJSON, áp products export endpoint Shop API.