Danh sách bài viết

Bài 6: JSON Format Trong REST

Bài 6 của series Rust RESTful API — đi sâu vào JSON Format theo RFC 8259: sáu type cơ bản (object, array, string, number, boolean, null), escape sequence, UTF-8 encoding bắt buộc; lý do JSON thắng XML cho REST API hiện đại; JSON Schema Draft 2020-12 và OpenAPI 3.1 để validate và document API (Rust qua schemars + utoipa); date format RFC 3339 với UTC; sự khác biệt null vs missing field cho PATCH partial update với pattern Option<Option<T>>; pitfall number precision (i64 max 2^63 vs JavaScript f64 safe 2^53) và pattern serialize big ID as string (Stripe, Twitter); lock policy cho Shop API về timestamp, ID, schema generation.

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

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

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

  • Nắm JSON cú pháp đầy đủ theo RFC 8259: sáu type cơ bản, escape sequence, UTF-8 encoding.
  • Hiểu vì sao JSON thắng XML cho web API hiện đại — compact, JavaScript native, schema optional, tool support universal.
  • Biết JSON Schema (Draft 2020-12) và OpenAPI 3.1 để validate cấu trúc và document API.
  • Nắm date format ISO 8601 / RFC 3339 với UTC, vì sao Unix timestamp và locale format đều là anti-pattern cho REST.
  • Hiểu sự khác biệt giữa nullmissing field — pitfall lớn khi serialize PATCH partial update.
  • Nắm pitfall number precision: Rust i64 max 2^63 - 1 nhưng JavaScript chỉ safe đến 2^53 - 1 — pattern fix là serialize big ID as string.
  • Áp dụng vào Shop API: response format JSON, RFC 3339 UTC cho timestamp, Option<Option<T>> cho PATCH, schemars + utoipa cho OpenAPI generation.
2

JSON Cú Pháp Recap (RFC 8259)

JSON (JavaScript Object Notation) chuẩn hóa qua RFC 8259 (xuất bản 12/2017, thay thế RFC 7159) và song song qua tiêu chuẩn ECMA-404. Format có sáu type cơ bản:

  • object: cặp khóa-giá trị bọc trong {}, khóa là string, giá trị là một trong sáu type — {"name": "Laptop", "price": 1499}.
  • array: danh sách giá trị có thứ tự bọc trong [][1, 2, 3] hoặc ["a", "b"].
  • string: chuỗi Unicode bọc trong dấu nháy kép — "laptop-y".
  • number: số nguyên hoặc số thực, không gắn type cụ thể trong spec — 42, 1499.00, -3.14e10.
  • boolean: true hoặc false (lowercase).
  • null: chỉ có giá trị null (lowercase) — biểu thị "không có giá trị".

String trong JSON cho phép một số escape sequence chuẩn: \" nháy kép, \\ backslash, \/ slash (optional), \n newline, \r carriage return, \t tab, \b backspace, \f form feed, và \uXXXX ký tự Unicode qua bốn chữ số hex (vd Á là ký tự "Á"). Mọi ký tự ngoài bảng escape phải được mã hóa trực tiếp qua UTF-8.

Number trong JSON là phần dễ gây nhầm. Spec cho phép cú pháp [-]int[.frac][exp], ví dụ 0, -17, 3.14, 2.5e10. Nhưng JSON không chấp nhận NaN, Infinity, -Infinity — những giá trị này hợp lệ trong IEEE 754 và trong JavaScript runtime, nhưng nếu xuất hiện trong JSON thì parser strict phải reject. Một số implementation (Python json default cho phép, JavaScript JSON.stringify in ra null) tùy chọn, nhưng spec là rõ ràng — đừng dựa vào.

Về encoding, RFC 8259 quy định UTF-8 là default tuyệt đối cho JSON trao đổi qua mạng giữa các hệ thống không cùng kiểm soát. UTF-16 và UTF-32 từng được phép trong RFC 7159 nhưng đã bị thu hẹp/loại bỏ ở RFC 8259. Header HTTP đi kèm thường là Content-Type: application/json; charset=utf-8 (đã lock cho Shop API ở B5).

JSON không cho phép trailing comma (dấu phẩy thừa cuối object/array) và không có comment. Hai ràng buộc này là khác biệt lớn với JSON5 và JSONC — hai dialect được tool dev (vscode settings.json, ESLint config) chấp nhận nhưng không phải JSON vanilla. Trao đổi qua REST API luôn dùng vanilla JSON.

{
  "id": 43,
  "slug": "laptop-y",
  "name": "Laptop Y",
  "price": 1499.00,
  "in_stock": true,
  "description": null,
  "tags": ["laptop", "portable", "office"],
  "specs": {
    "cpu": "Intel Core i7-13700H",
    "ram_gb": 16,
    "weight_kg": 1.45
  }
}

Ví dụ trên dùng đủ sáu type: object lồng, array of string, string thường, number (cả integer và float), boolean, null. Đây là dạng response chuẩn cho GET /api/v1/products/43 mà Shop API sẽ trả.

3

Tại Sao JSON Thắng XML?

Đầu những năm 2000, XML là format chuẩn cho web service (SOAP, XML-RPC). Từ 2006-2010, JSON nhanh chóng thay thế XML cho phần lớn REST API. Lý do không nằm ở một điểm duy nhất mà là sự cộng hưởng của nhiều yếu tố.

Tiêu chí                JSON                                XML
Cú pháp                 {"name":"X","age":30}               <user><name>X</name><age>30</age></user>
Kích thước (xấp xỉ)     22 byte                             45 byte (gấp đôi)
JavaScript parse        JSON.parse() built-in từ ES5        DOMParser, phức tạp
Schema bắt buộc         Không (optional qua JSON Schema)    Có thể (XSD, DTD)
Namespace               Không có                            xmlns:foo phức tạp
Comment                 Không hỗ trợ                        <!-- ... --> hỗ trợ
Attribute               Không có (chỉ key-value)            Có (<tag attr="...">)
Tool ecosystem REST     Universal (serde, jackson, json)    Đầy đủ nhưng overkill
Use case mạnh           Web API, config, log                Document-centric, SOAP, RSS

Compact là yếu tố trực giác đầu tiên. JSON ngắn hơn XML khoảng 30-50% cho cùng dữ liệu do không có thẻ mở-đóng. Trên payload lớn (vd response list 1000 sản phẩm), khác biệt là MB chứ không phải byte — ảnh hưởng băng thông và latency.

JavaScript native: JSON.parse()JSON.stringify() là API built-in của trình duyệt từ ECMAScript 5 (2009). Single-page app gọi REST API không cần thư viện thứ ba để parse response. XML cần DOMParser với cú pháp dài và xử lý namespace phức tạp.

Schema optional: JSON cho phép gửi body không có schema enforcement, linh hoạt cho startup và prototype. Khi cần strict, JSON Schema là tùy chọn (bước 4). XML có XSD/DTD nhưng strict mặc định, tốn công setup.

Không namespace: XML namespace (xmlns:foo) sinh ra để hỗ trợ document trộn từ nhiều schema, nhưng phần lớn REST API không cần — namespace chỉ thêm noise. JSON không có khái niệm tương đương, đơn giản hơn.

Tool support: Mọi ngôn ngữ hiện đại có thư viện JSON tốt — Rust serde_json, Python json, Java Jackson / Gson, Go encoding/json, C# System.Text.Json. XML cũng có lib nhưng nặng hơn và ít dùng cho REST.

XML vẫn giữ chỗ trong một số domain: SOAP web service enterprise (thanh toán liên ngân hàng, EDI), document-centric (RSS feed, sitemap, OOXML), config legacy (Maven pom.xml, Spring XML). Nhưng REST API hiện đại — bao gồm Shop API — JSON gần như là mặc định tuyệt đối. Quyết định JSON-only cho mọi data endpoint của Shop API đã được lock ở B5.

4

JSON Schema — Validate Cấu Trúc

JSON Schema là spec mô tả cấu trúc của JSON document. Phiên bản hiện hành là Draft 2020-12 (xuất bản 06/2022). Schema được viết bằng JSON, định nghĩa: type của từng field, required fields, pattern regex, enum giá trị hợp lệ, min/max cho number/string/array.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "ProductDto",
  "type": "object",
  "required": ["name", "price"],
  "properties": {
    "name": {
      "type": "string",
      "minLength": 3,
      "maxLength": 200
    },
    "price": {
      "type": "number",
      "minimum": 0,
      "exclusiveMaximum": 1000000
    },
    "currency": {
      "type": "string",
      "enum": ["USD", "VND", "EUR"]
    },
    "description": {
      "type": ["string", "null"]
    }
  },
  "additionalProperties": false
}

Schema trên nói: ProductDto là object, bắt buộc có nameprice; name dài 3-200 ký tự; price là số ≥ 0 và < 1,000,000; currency chỉ chấp nhận một trong ba mã; description nullable; field khác không cho phép.

Use case của JSON Schema trong REST API: (1) validate request body trước khi parse vào struct, trả lỗi 422 chi tiết nếu sai; (2) document contract cho client biết nên gửi gì; (3) generate code (TypeScript interface, Rust struct) từ schema; (4) là nền tảng cho OpenAPI ở phần component schema.

Trong Rust, có hai cách tiếp cận. Crate jsonschema dùng để validate runtime — bạn có schema dưới dạng serde_json::Value, dùng compiler để parse, rồi gọi is_valid() hoặc validate(). Crate schemars đi hướng ngược lại — derive macro tự sinh schema từ struct Rust:

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct ProductDto {
    /// Tên sản phẩm, 3-200 ký tự.
    #[schemars(length(min = 3, max = 200))]
    pub name: String,

    /// Giá sản phẩm, >= 0.
    #[schemars(range(min = 0.0))]
    pub price: f64,

    /// Mã tiền tệ ISO 4217.
    pub currency: Currency,

    /// Mô tả chi tiết (nullable).
    pub description: Option<String>,
}

Shop API plan: dùng schemars kết hợp utoipa để auto-generate OpenAPI spec từ struct DTO và axum handler. Chi tiết implementation ở B8.

5

OpenAPI 3.1 — Document REST API

OpenAPI (trước 2016 tên là Swagger) là spec mô tả REST API toàn diện: endpoint, method, request/response schema, query parameter, authentication, error response. Phiên bản hiện hành là OpenAPI 3.1 (xuất bản 02/2021), align với JSON Schema Draft 2020-12 — nghĩa là schema component trong OpenAPI JSON Schema, tái sử dụng được cross-tool.

OpenAPI spec là một file YAML hoặc JSON lớn, thường tên openapi.json. Từ spec đó, ecosystem tool sinh ra: Swagger UI (giao diện browse và test API), Postman collection (import vào Postman để test), Redoc (HTML doc đẹp), client SDK đa ngôn ngữ (TypeScript, Rust, Go).

Trong Rust + axum, crate chuẩn de facto là utoipa. Macro #[utoipa::path] đặt lên handler để khai báo metadata (path, method, request body, response code, schema component); macro #[derive(ToSchema)] đặt lên struct DTO để sinh schema.

use axum::{extract::Path, Json};
use utoipa::ToSchema;

#[derive(serde::Serialize, ToSchema)]
pub struct Product {
    pub id: i64,
    pub slug: String,
    pub name: String,
    pub price: f64,
}

#[utoipa::path(
    get,
    path = "/api/v1/products/{id}",
    params(
        ("id" = i64, Path, description = "ID sản phẩm"),
    ),
    responses(
        (status = 200, description = "Tìm thấy sản phẩm", body = Product),
        (status = 404, description = "Không tồn tại"),
    ),
    tag = "Products",
)]
pub async fn get_product(Path(id): Path<i64>) -> Json<Product> {
    // ... fetch from DB
    todo!()
}

Khi project setup xong, Shop API sẽ expose hai endpoint hỗ trợ: GET /api-doc/openapi.json trả spec (đã lock từ endpoint overview), và GET /swagger-ui serve giao diện Swagger UI để dev browse và test trực tiếp trên browser. Cả hai chỉ bật ở dev/staging — production tắt để tránh leak schema chi tiết.

Có hai trường phái cho API documentation. API-first viết OpenAPI spec trước, generate code sau (Rust struct, client SDK) — phù hợp team có API contract rõ trước khi build. Code-first viết code trước, generate spec từ code (qua macro utoipa) — phù hợp team Rust agile, schema theo đúng code thật. Shop API chọn code-first với utoipa để tránh drift giữa spec và implementation. Chi tiết ở B8.

6

Date Format: ISO 8601 (RFC 3339)

JSON không có native date type. Date phải được encode dưới dạng string (hoặc number, nhưng sẽ thấy đây là anti-pattern). Câu hỏi là: dùng format string nào?

Standard quốc tế là ISO 8601, định nghĩa lần đầu năm 1988, ấn bản hiện hành 2019. ISO 8601 đặc tả format date-time đầy đủ với nhiều biến thể. Cho REST API, subset stricter và rõ ràng hơn là RFC 3339 (xuất bản 07/2002) — RFC 3339 chính là phần ISO 8601 mà internet protocol dùng, đơn giản hóa và loại bỏ ambiguity.

Loại                Ví dụ                                Ghi chú
Date-time UTC       2026-06-12T14:30:00Z                  "Z" = Zulu = UTC
Date-time + offset  2026-06-12T14:30:00+07:00             Có offset rõ ràng
Date-time với ms    2026-06-12T14:30:00.123Z              Có millisecond
Date only           2026-06-12                            Không có time
Time only           14:30:00                              Không có date
ANTI-PATTERN        1718203800                            Unix timestamp
ANTI-PATTERN        12/06/2026 14:30                      Locale, ambiguous
ANTI-PATTERN        Jun 12, 2026 2:30 PM                  Human-readable, locale

Cú pháp đầy đủ: YYYY-MM-DDTHH:MM:SS kèm offset (Z cho UTC, hoặc +07:00 / -05:00 cho timezone offset cụ thể). Phần phân số giây tùy chọn (.123 cho millisecond, .123456 cho microsecond).

Vì sao tránh Unix timestamp (số giây kể từ epoch 1970-01-01 UTC)? Ba lý do. Một, mất tính human-readable — đọc log thấy 1718203800 không biết là ngày nào, phải convert. Hai, mất thông tin timezone — Unix timestamp luôn là UTC nhưng không tự ghi rõ, dễ nhầm khi format hiển thị ở client. Ba, không có khái niệm "date only" — không thể biểu diễn "ngày sinh là 12/06/2000" không kèm time.

Vì sao tránh locale format như 12/06/2026? Đơn giản: ambiguous. 12/06/2026 ở châu Âu (DMY) là 12 tháng 6, ở Mỹ (MDY) là tháng 12 ngày 6. REST API là contract giữa client và server qua mạng, không có context locale chung — bắt buộc dùng format không ambiguous. RFC 3339 với pattern YYYY-MM-DD không nhầm lẫn được.

Shop API lock policy: dùng RFC 3339 với UTC (suffix Z) cho mọi timestamp trao đổi qua API — created_at, updated_at, order_date, verified_at, ... Lý do chọn UTC: lưu trữ và truyền tải nhất quán, không bị ảnh hưởng bởi daylight saving time, không phải đồng bộ timezone giữa các server. Format locale (hiển thị "12/06/2026 21:30") là client-side concern — frontend tự convert UTC → local timezone của user khi render.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct Order {
    pub id: i64,
    pub user_id: i64,
    pub created_at: DateTime<Utc>,   // Tự serialize "2026-06-12T14:30:00Z"
    pub updated_at: DateTime<Utc>,
}

Rust có hai crate phổ biến: chronotime. chrono::DateTime<Utc> serialize ra RFC 3339 mặc định khi feature serde bật. time::OffsetDateTime là alternative hiện đại, không dùng panic trong API. Shop API chọn chrono::DateTime<Utc> (chi tiết B44) vì ecosystem axum/sqlx hỗ trợ tốt và đa số dev Rust đã quen.

7

null vs Missing Field — Pitfall Partial Update

Cùng một field description trong JSON request body có thể xuất hiện ở ba trạng thái khác nhau, mỗi trạng thái mang ngữ nghĩa khác hẳn nhau khi xử lý PATCH partial update:

// Case 1: field missing — client không nhắc tới
{ "name": "Laptop Y" }

// Case 2: field explicit null — client gửi null
{ "name": "Laptop Y", "description": null }

// Case 3: field có giá trị — client gửi string mới
{ "name": "Laptop Y", "description": "Laptop văn phòng cao cấp" }

Ngữ nghĩa REST chuẩn:

  • Missing nghĩa là "đừng động vào field này, giữ nguyên giá trị hiện tại".
  • Explicit null nghĩa là "set field này về NULL trong database (xóa mô tả hiện có)".
  • Giá trị nghĩa là "thay thế giá trị cũ bằng giá trị mới".

Pitfall: cách deserialize mặc định của serde trong Rust không phân biệt hai case đầu. Nếu DTO định nghĩa description: Option<String> với #[serde(default)], cả missing và explicit null đều thành None sau deserialize. Server không có cách biết client muốn "giữ nguyên" hay "xóa".

Pattern fix là dùng double Option: Option<Option<T>>. Outer Option phân biệt missing (None) vs hiện diện (Some), inner Option phân biệt null (Some(None)) vs giá trị (Some(Some(v))):

use serde::{Deserialize, Serialize};
use serde_with::{serde_as, rust::double_option};

#[serde_as]
#[derive(Debug, Deserialize)]
pub struct PatchProductDto {
    /// None = không nhắc tới (giữ nguyên).
    /// Some(None) = explicit null (xóa).
    /// Some(Some(s)) = giá trị mới.
    #[serde(default, with = "::serde_with::rust::double_option")]
    pub description: Option<Option<String>>,

    #[serde(default, with = "::serde_with::rust::double_option")]
    pub price: Option<Option<f64>>,
}

pub async fn patch_product(
    Path(id): Path<i64>,
    Json(dto): Json<PatchProductDto>,
) -> Result<Json<Product>, AppError> {
    // dto.description:
    //   None       → bỏ qua, không UPDATE description trong SQL
    //   Some(None) → UPDATE products SET description = NULL WHERE id = ?
    //   Some(Some) → UPDATE products SET description = ? WHERE id = ?
    todo!()
}

Crate serde_with cung cấp helper rust::double_option để serde tự sinh code đúng — không phải viết custom deserializer tay.

Shop API lock policy: endpoint PATCH (vd PATCH /api/v1/me, PATCH /api/v1/admin/products/:id) dùng Option<Option<T>> cho mọi field nullable để phân biệt đầy đủ ba case. Endpoint PUT replace nhận full body nên không cần — null tương đương "set null", missing tương đương "không có field, validation fail". Chi tiết implementation PATCH partial ở B66.

8

Number Precision Pitfall: i64 vs f64 (JavaScript Issue)

JSON spec không phân biệt integer và float — number là number, miễn parse được theo cú pháp [-]int[.frac][exp]. Cách representation cụ thể tùy implementation phía parser. Đây là nguồn gốc của một pitfall lớn khi tích hợp với JavaScript/browser client.

JavaScript chỉ có một number type duy nhất, là IEEE 754 double-precision float (f64). f64 dành 52 bit cho mantissa, tức max safe integer là 2^53 - 1 = 9_007_199_254_740_991 (khoảng 9 × 1015). Số nguyên lớn hơn vẫn được biểu diễn nhưng mất chính xác — hai số nguyên kề nhau có thể bị parse thành cùng một f64.

Type                Max value                                Bits
Rust i32            2_147_483_647 (~2.1 × 10^9)              32
Rust i64            9_223_372_036_854_775_807 (~9.2 × 10^18) 64
JS f64 safe int     9_007_199_254_740_991 (~9.0 × 10^15)     52 (mantissa)
JS f64 max          1.7976931348623157e+308                  64
Tỉ lệ i64 / safe    ~1024 lần (10 chữ số)

Vấn đề trong REST API: server Rust trả id: 9_223_372_036_854_775_000 (i64 hợp lệ) trong JSON. Browser JavaScript parse qua JSON.parse() thành f64, và giá trị bị round thành 9_223_372_036_854_775_000 hoặc 9_223_372_036_854_776_000 tùy rounding mode — không phân biệt được. Bug âm thầm, không có error, dữ liệu bị sai.

JS: JSON.parse('{"id": 9007199254740993}').id
=> 9007199254740992   // mất chính xác 1!

JS: JSON.parse('{"id": "9007199254740993"}').id
=> "9007199254740993" // string, an toàn

Pattern fix được Stripe, Twitter/X, GitHub, và phần lớn API có ID lớn áp dụng: serialize big number dưới dạng string trong JSON. Ví dụ Twitter ID dùng snowflake 64-bit, response trả cả hai dạng:

{
  "id": 1395239835942625280,
  "id_str": "1395239835942625280"
}

Trong Rust với serde, có thể dùng attribute để serialize integer thành string:

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct Tweet {
    /// Serialize as string trong JSON để JS parse an toàn.
    #[serde(with = "serde_with::rust::display_fromstr")]
    pub id: i64,

    pub author_id: i64,
    pub content: String,
}

Shop API policy ID: dùng BIGSERIAL (i64) của PostgreSQL cho mọi primary key (đã ghi ở database column convention). Trong realistic case, ID Shop API tăng tuần tự từ 1, sẽ không bao giờ vượt 2^53 - 1 ngay cả sau hàng trăm năm vận hành — nên serialize as number thường trong JSON là an toàn. Nếu sau này quyết định chuyển sang UUID (B68 sẽ chốt), UUID đã là string mặc định (vd "550e8400-e29b-41d4-a716-446655440000") nên không gặp vấn đề. Money và quantity dùng rust_decimal::Decimal (chi tiết B44), serialize as string để tránh float precision loss với phép tính tiền.

9

Tổng Kết

  • JSON RFC 8259: sáu type cơ bản (object, array, string, number, boolean, null), UTF-8 mặc định, không trailing comma, không comment, không NaN/Infinity.
  • JSON thắng XML cho REST API hiện đại: compact (~50% ngắn hơn), JavaScript native qua JSON.parse, schema optional, không namespace, tool support universal mọi ngôn ngữ.
  • JSON Schema Draft 2020-12 validate cấu trúc (required, type, pattern, enum, min/max); OpenAPI 3.1 document API hoàn chỉnh — Rust dùng schemars + utoipa generate spec từ struct DTO và axum handler (chi tiết B8).
  • Date dùng RFC 3339 với UTC: 2026-06-12T14:30:00Z; tránh Unix timestamp (mất human-readable, mất timezone info) và locale format (ambiguous DMY vs MDY).
  • null khác missing field: missing = giữ nguyên, explicit null = set NULL, giá trị = thay thế — ba case khác nhau cho PATCH partial update; pattern fix Rust là Option<Option<T>> qua serde_with::rust::double_option.
  • Number precision: JavaScript f64 safe integer là 2^53 - 1 (~9 × 1015), Rust i64 max là 2^63 - 1 (~9.2 × 1018), big i64 cần serialize as string trong JSON (pattern Stripe, Twitter, GitHub) để JS client parse an toàn.
  • Shop API lock: JSON-only mọi data endpoint, RFC 3339 UTC cho timestamp (chrono::DateTime<Utc>), ID i64 BIGSERIAL serialize as number (an toàn vì < 2^53), money dùng rust_decimal::Decimal serialize as string, PATCH dùng Option<Option<T>> cho nullable field. UUID alternative cho ID sẽ chốt ở B68.
10

Bài Tập Củng Cố

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

  1. JSON có hỗ trợ NaN, Infinity, hoặc comment không? Đúng/sai và giải thích theo RFC 8259.
  2. Vì sao JSON thắng XML cho REST API hiện đại? Liệt kê ba lý do mạnh nhất.
  3. Date 12/06/2026 14:30 là ambiguous. Format đúng cho REST API là gì? Vì sao dùng UTC thay vì giữ timezone của server?
  4. Trong PATCH /api/v1/products/:id, client gửi {"description": null}{} có khác nhau không? Server phải xử lý thế nào để phân biệt? Pattern Rust nào dùng?
  5. Tại sao Rust i64 max là ~9.2 × 1018 nhưng JavaScript chỉ safe đến ~9 × 1015? Pattern fix khi server trả ID lớn cho browser client là gì?
Đáp án
  1. Không. Spec RFC 8259 không cho phép NaN, Infinity, -Infinity trong JSON dù những giá trị này hợp lệ trong IEEE 754 và JavaScript runtime — parser strict phải reject. Cũng không cho phép comment (cả // lẫn /* */). Hai dialect JSON5 và JSONC cho phép comment và trailing comma nhưng đó là superset không chuẩn, chỉ dùng cho config file (vscode settings.json), không dùng cho REST API. Implementation Python json default cho phép NaN/Infinityallow_nan=True nhưng đó là phá vỡ tương thích, đừng dựa vào. JavaScript JSON.stringify(NaN) trả về "null", không phải "NaN" — đúng spec.
  2. Ba lý do mạnh nhất: (1) Compact — JSON ngắn hơn XML khoảng 30-50% do không có thẻ mở-đóng và attribute. Trên payload list lớn, khác biệt là MB ảnh hưởng băng thông và latency. (2) JavaScript nativeJSON.parse()JSON.stringify() built-in từ ES5 (2009), single-page app không cần thư viện thứ ba. XML cần DOMParser với cú pháp phức tạp. (3) Schema optional + tool universal — JSON cho phép gửi body không enforce schema, linh hoạt; khi cần strict thì JSON Schema là tùy chọn. Mọi ngôn ngữ hiện đại có lib JSON tốt (Rust serde_json, Python json, Java Jackson, Go encoding/json) — XML cũng có nhưng nặng và ít dùng cho REST. XML vẫn giữ chỗ ở SOAP enterprise, RSS, document-centric — không phải REST modern.
  3. Format đúng cho REST API là RFC 3339 với UTC: 2026-06-12T14:30:00Z (suffix Z = Zulu = UTC). RFC 3339 là subset stricter của ISO 8601, không ambiguous (pattern YYYY-MM-DD không nhầm với DMY/MDY). Dùng UTC thay vì timezone server vì: (a) nhất quán giữa các server (US-East, EU-West, AP-Southeast đều dùng cùng giờ UTC, log so sánh thẳng được); (b) không bị daylight saving (timezone như "America/New_York" có DST khiến 2 giờ sáng xuất hiện hai lần trong năm); (c) tách concern — display theo timezone của user là client-side concern, frontend tự convert UTC → local. Anti-pattern Unix timestamp 1718203800 mất human-readable. Anti-pattern locale 12/06/2026 ambiguous DMY vs MDY. Rust dùng chrono::DateTime<Utc>, serde feature mặc định serialize ra RFC 3339.
  4. Khác nhau hoàn toàn. {} nghĩa là client không nhắc tới description — server phải giữ nguyên giá trị hiện tại trong database (không UPDATE column này). {"description": null} nghĩa là client gửi explicit null — server phải set NULL trong database (UPDATE products SET description = NULL). Nếu client gửi {"description": "new text"} thì thay thế bằng giá trị mới. Pitfall: serde mặc định với Option<String> + #[serde(default)] deserialize cả missing và null thành None — mất khả năng phân biệt. Pattern Rust fix là Option<Option<T>> qua helper serde_with::rust::double_option: outer None = missing, outer Some(None) = explicit null, outer Some(Some(v)) = giá trị. Shop API lock pattern này cho mọi PATCH endpoint nullable field; PUT replace nhận full body không cần. Chi tiết implementation ở B66.
  5. JavaScript chỉ có một number type duy nhất là IEEE 754 double-precision float (f64). f64 dành 52 bit cho mantissa, max safe integer là 2^53 - 1 = 9_007_199_254_740_991 (~9 × 1015). Rust i64 là 64-bit signed integer riêng, max 2^63 - 1 = 9_223_372_036_854_775_807 (~9.2 × 1018, gấp 1024 lần). Khoảng giữa 2^532^63 là vùng "không an toàn cho JS": JavaScript vẫn parse được nhưng mất chính xác vì f64 không đủ mantissa, hai số nguyên kề nhau có thể bị round vào cùng một f64. Bug âm thầm, không error. Pattern fix: serialize big integer ID dưới dạng string trong JSON: "id": "9007199254740993" thay vì "id": 9007199254740993. Stripe, Twitter/X, GitHub đều dùng pattern này. Rust serde: #[serde(with = "serde_with::rust::display_fromstr")]. Shop API lock: ID dùng i64 BIGSERIAL serialize as number thường vì realistic case ID < 2^53 (tăng tuần tự từ 1, không bao giờ chạm 9 × 1015); money + quantity dùng rust_decimal::Decimal serialize as string để tránh float precision; UUID alternative ID là string mặc định, sẽ chốt ở B68.
11

Bài Tiếp Theo

— so sánh sâu ba architectural style: REST resource-based, RPC contract-first (gRPC, JSON-RPC), GraphQL flexible query. Bảng trade-off, use case mỗi loại, vì sao series chọn REST.