JSON sang Python: chọn giữa dataclass, TypedDict và Pydantic, kèm bẫy giá trị mặc định thay đổi được
Dán một khối JSON, công cụ sinh lớp Python theo một trong ba mô hình là dataclass, TypedDict hoặc model Pydantic phiên bản hai. Nó sinh gợi ý kiểu đúng cú pháp hiện đại, đổi tên khóa sang snake_case kèm alias giữ tên gốc, đặt default_factory cho kiểu thay đổi được và xếp lớp con lên trước lớp cha.
Tính năng nổi bật
- Ba kiểu đầu ra để chọn: dataclass, TypedDict và model Pydantic phiên bản hai
- Gợi ý kiểu theo cú pháp hiện đại: list ngoặc vuông, dict ngoặc vuông, Optional cho trường có thể rỗng
- Đổi tên khóa sang snake_case và giữ tên gốc trong alias hoặc trong metadata tùy kiểu đầu ra
- Tự đặt default_factory cho danh sách và từ điển, không bao giờ đặt giá trị thay đổi được làm mặc định
- Sắp xếp lại trường để trường không có mặc định luôn đứng trước trường có mặc định
- Xếp lớp con lên trước lớp cha nên tệp chạy được ngay không cần khai báo tham chiếu tới trước
- Tự gom các dòng nhập gói cần thiết từ typing, dataclasses, datetime và pydantic
- Khóa không phải định danh Python hợp lệ được chuyển sang cú pháp hàm của TypedDict
Vì sao dán JSON vào rồi tự gõ lớp Python lại hay hỏng ở ba chỗ giống nhau
Người viết Python quen với việc đọc thẳng phản hồi dưới dạng từ điển rồi lấy giá trị bằng ngoặc vuông, và cách đó chạy được cho tới lúc dự án lớn lên. Khi đó ba vấn đề xuất hiện gần như đồng loạt. Thứ nhất, không công cụ nào gợi ý được tên khóa cho bạn, nên gõ sai một chữ chỉ lộ ra lúc chạy dưới dạng lỗi thiếu khóa. Thứ hai, tên khóa trong JSON thường viết kiểu lạc đà còn quy ước Python là gạch dưới, nên mã của bạn lẫn hai lối viết và người đọc sau không biết đâu là dữ liệu ngoài đâu là dữ liệu trong. Thứ ba, khi bạn bắt đầu tự gõ lớp, ba cái bẫy quen thuộc của Python ập tới cùng lúc là giá trị mặc định thay đổi được, thứ tự trường có mặc định, và tham chiếu tới lớp chưa được định nghĩa. Công cụ này xử lý sẵn cả ba, để bạn chỉ còn phải quyết định phần thuộc về thiết kế là chọn mô hình nào cho lớp dữ liệu.
Lợi ích khi sử dụng
- Có ngay lớp chạy được, không vướng lỗi thứ tự trường hay lỗi mặc định thay đổi được
- Trình soạn thảo gợi ý được tên trường nên hết lỗi gõ sai tên khóa
- Tên trong mã theo đúng quy ước Python mà vẫn nhận đúng tên khóa gốc từ dữ liệu ngoài
- So sánh trực tiếp ba mô hình lớp dữ liệu trên cùng một khối JSON để chọn cho đúng
- Chạy hoàn toàn trên trình duyệt nên dán được phản hồi của hệ thống nội bộ
Cách sinh lớp Python từ một khối JSON
- 1Dán phần thân phản hồi thật vào ô bên trái, nên lấy từ tab mạng hoặc từ lệnh gọi thật thay vì chép trong tài liệu.
- 2Chọn kiểu đầu ra: dataclass cho cấu trúc nội bộ, TypedDict khi vẫn muốn làm việc với từ điển, Pydantic khi cần kiểm định lúc chạy.
- 3Đặt tên lớp gốc theo tên tài nguyên, và bật hoặc tắt việc đổi tên khóa sang snake_case kèm alias.
- 4Bấm sinh lớp, đọc từ trên xuống, lớp con nằm trước và lớp gốc nằm cuối cùng.
- 5Rà lại trường mang giá trị rỗng trong mẫu, vì kiểu của chúng chỉ là Optional của Any cho tới khi bạn điền kiểu thật.
Ba mô hình, ba mức đảm bảo khác nhau, chọn theo việc chứ đừng chọn theo thói quen
dataclass là thứ có sẵn trong thư viện chuẩn, nó sinh giúp bạn phương thức khởi tạo, phương thức so sánh và phương thức hiển thị, đổi lại nó hoàn toàn không kiểm tra kiểu lúc chạy. Bạn truyền chuỗi vào một trường khai báo là số nguyên thì nó nhận, và lỗi chỉ nổ ra ở chỗ nào đó rất xa. Vì vậy dataclass hợp nhất với dữ liệu đã sạch, tức các cấu trúc bạn tự tạo bên trong chương trình, chứ không phải dữ liệu vừa nhận từ bên ngoài. TypedDict thì đặc biệt hơn cả: nó không tạo ra lớp thật lúc chạy, đối tượng của bạn vẫn là từ điển bình thường, chỉ có công cụ kiểm tra kiểu tĩnh mới nhìn thấy khai báo. Ưu điểm là không tốn chi phí chuyển đổi và không phải sửa mã cũ đang truy cập bằng ngoặc vuông, nhược điểm là không có kiểm định, không có phương thức, và quan trọng nhất là không đổi được tên khóa vì khóa chính là dữ liệu. Pydantic phiên bản hai đi xa nhất: nó kiểm tra và ép kiểu ngay lúc khởi tạo, ném ra lỗi mô tả rõ trường nào sai, hỗ trợ alias để nhận tên khóa gốc, hỗ trợ hàm kiểm tra riêng cho từng trường. Cái giá là một phụ thuộc bên ngoài và một chút chi phí lúc chạy. Quy tắc chọn gọn nhất: dữ liệu vào từ ngoài thì dùng Pydantic ở đúng ranh giới, cấu trúc nội bộ thì dùng dataclass, còn mã cũ đang xử lý từ điển mà bạn chỉ muốn thêm gợi ý kiểu thì dùng TypedDict.
Giá trị mặc định thay đổi được là cái bẫy kinh điển của Python
Nếu bạn viết một lớp có trường danh sách rồi gán thẳng dấu ngoặc vuông rỗng làm giá trị mặc định, danh sách đó được tạo đúng một lần tại thời điểm định nghĩa lớp, và mọi thể hiện tạo về sau đều dùng chung chính danh sách ấy. Thêm một phần tử vào đối tượng thứ nhất thì đối tượng thứ hai cũng thấy phần tử đó xuất hiện, dù hai đối tượng chẳng liên quan gì tới nhau. Cùng chuyện đó xảy ra với từ điển và với tập hợp. Đây là một trong những lỗi khó lần ra nhất vì triệu chứng xuất hiện rất xa nguyên nhân, thường dưới dạng dữ liệu của người dùng này lẫn sang người dùng khác. Trong dataclass, việc gán thẳng như vậy còn bị chặn ngay lúc định nghĩa lớp, chương trình ném lỗi luôn, đó là điều may. Cách đúng là dùng hàm nhà máy: khai báo default_factory bằng chính tên kiểu, khi đó mỗi thể hiện gọi hàm một lần và nhận một đối tượng mới. Công cụ này luôn sinh default_factory cho danh sách và từ điển ở cả hai kiểu đầu ra dataclass và Pydantic. Có một ngoại lệ cố ý: lớp lồng không được đặt hàm nhà máy, vì gọi lớp đó với danh sách tham số rỗng sẽ vỡ ngay nếu bên trong còn trường bắt buộc. Lớp lồng vì vậy luôn là trường bắt buộc, và đó cũng là cách mô tả đúng thực tế, bởi một đối tượng con có mặt trong dữ liệu thì nó phải có mặt.
Đổi tên khóa sang snake_case và giữ tên gốc bằng alias
Quy ước đặt tên của Python là chữ thường ngăn bằng gạch dưới, trong khi phần lớn giao diện lập trình trả về khóa viết kiểu lạc đà. Nếu bạn giữ nguyên tên khóa gốc làm tên thuộc tính, mã Python của bạn sẽ lẫn hai lối viết và công cụ kiểm tra phong cách sẽ nhắc liên tục. Nếu bạn đổi tên mà không lưu tên gốc, bạn mất khả năng đọc thẳng từ dữ liệu ngoài. Giải pháp là đổi tên đồng thời khai báo tên gốc, nhưng ba mô hình lại hỗ trợ chuyện này ở ba mức khác nhau. Với Pydantic, đây là tính năng chính thức: mỗi trường nhận một alias là tên khóa gốc, và lớp được cấu hình cho phép khởi tạo bằng cả tên gốc lẫn tên đã đổi, nhờ vậy việc nhận dữ liệu và việc viết mã kiểm thử đều thuận. Với dataclass, thư viện chuẩn không có khái niệm alias, nên công cụ ghi tên gốc vào phần metadata của trường, đây là chỗ chứa dữ liệu kèm theo mà thư viện không dùng tới nhưng mã của bạn đọc được để tự dựng hàm ánh xạ. Đó là quy ước chứ không phải cơ chế, tức là bạn vẫn phải tự viết đoạn chuyển từ từ điển sang lớp. Với TypedDict thì đơn giản là không đổi tên được: khóa trong khai báo chính là khóa trong từ điển thật, đổi tên là đổi luôn dữ liệu. Vì vậy khi bạn chọn kiểu đầu ra TypedDict, công cụ tự tắt công tắc đổi tên và giữ nguyên khóa gốc.
Thứ tự trường, tham chiếu tới trước và các luật bắt buộc của Python
Hai luật của Python quyết định thứ tự trong tệp sinh ra, và cả hai đều gây lỗi ngay lúc nạp tệp nếu vi phạm. Luật thứ nhất áp cho dataclass: mọi trường có giá trị mặc định phải nằm sau tất cả trường không có mặc định, vì phương thức khởi tạo sinh ra nhận tham số theo thứ tự khai báo và tham số có mặc định không được đứng trước tham số không có. Vi phạm thì chương trình ném lỗi ngay tại dòng định nghĩa lớp. Vì vậy công cụ sắp xếp lại trường, đưa nhóm bắt buộc lên trước nhóm có mặc định, và điều này khiến thứ tự trong mã sinh ra có thể khác thứ tự khóa trong JSON của bạn. Nếu bạn cần giữ nguyên thứ tự gốc vì lý do trình bày, hãy chọn kiểu đầu ra Pydantic, ở đó luật này không áp dụng theo cách như vậy. Luật thứ hai áp cho mọi kiểu đầu ra: một tên lớp phải được định nghĩa trước khi nó được dùng làm gợi ý kiểu ở nơi khác, trừ khi bạn dùng chuỗi hoặc bật chế độ hoãn đánh giá chú thích. Công cụ chọn cách đơn giản và tường minh nhất là xếp lớp con lên trước lớp cha, nên tệp sinh ra nạp được ngay ở mọi phiên bản Python có sẵn cú pháp gợi ý kiểu dựng sẵn. Cần lưu ý cú pháp danh sách và từ điển viết bằng chữ thường kèm ngoặc vuông chỉ dùng trực tiếp được từ Python 3.9 trở đi, và cú pháp NotRequired trong TypedDict cần Python 3.11 hoặc gói mở rộng typing bên ngoài.
Sinh lớp xong vẫn chưa phải là đã kiểm tra dữ liệu, và ranh giới với các trang khác
Chỉ có kiểu đầu ra Pydantic thật sự kiểm tra dữ liệu lúc chạy. Với dataclass và TypedDict, gợi ý kiểu chỉ là chú thích: chúng giúp trình soạn thảo gợi ý và giúp công cụ kiểm tra tĩnh bắt lỗi khi bạn phân tích mã, nhưng lúc chương trình chạy thì không ai kiểm tra gì cả. Nghĩa là nếu máy chủ đổi một trường từ số sang chuỗi, chương trình của bạn vẫn chạy tiếp và vỡ ở một chỗ hoàn toàn khác, khó lần ngược. Vì vậy nguyên tắc thực dụng là kiểm định ngay tại ranh giới nhận dữ liệu rồi bên trong dùng cấu trúc đã sạch. Về ranh giới với các trang khác trên site: trang này chỉ sinh khai báo lớp, nó không sinh mã gọi mạng, không sinh mã ánh xạ cơ sở dữ liệu và không kiểm định dữ liệu của bạn. Nếu ngôn ngữ đích của bạn là Go, nơi vấn đề trung tâm là con trỏ và thẻ json, hãy dùng /vi/tools/json-to-go-struct. Nếu là TypeScript, nơi kiểu biến mất hoàn toàn lúc chạy, hãy dùng /vi/tools/json-to-typescript. Nếu bạn cần một bản mô tả cấu trúc độc lập ngôn ngữ để dùng cho kiểm định ở nhiều nơi, hãy dựng lược đồ tại /vi/tools/json-schema-generator. Còn nếu khối JSON bạn đang có vẫn còn chú thích hay dấu phẩy thừa, hãy làm sạch trước tại /vi/tools/json5-converter vì trang này dùng bộ đọc JSON chặt.
Câu hỏi thường gặp (FAQ)
Nên chọn dataclass, TypedDict hay Pydantic?
Dữ liệu vừa nhận từ bên ngoài thì dùng Pydantic vì chỉ nó kiểm tra và ép kiểu lúc chạy. Cấu trúc nội bộ do chính chương trình bạn tạo thì dataclass là đủ và không cần thêm phụ thuộc. Mã cũ đang xử lý từ điển mà bạn chỉ muốn thêm gợi ý kiểu thì TypedDict là lựa chọn ít xâm lấn nhất.
Vì sao thứ tự trường trong lớp sinh ra khác thứ tự khóa trong JSON của tôi?
Vì dataclass bắt buộc mọi trường có giá trị mặc định phải đứng sau các trường không có mặc định, vi phạm là chương trình ném lỗi ngay lúc định nghĩa lớp. Công cụ sắp xếp lại theo luật này. Nếu bạn cần giữ nguyên thứ tự gốc, hãy chọn kiểu đầu ra Pydantic.
Vì sao danh sách phải dùng default_factory thay vì gán thẳng ngoặc vuông rỗng?
Vì danh sách gán thẳng chỉ được tạo một lần lúc định nghĩa lớp, nên mọi thể hiện dùng chung một đối tượng và dữ liệu lẫn sang nhau. Hàm nhà máy tạo một đối tượng mới cho mỗi thể hiện. Riêng dataclass còn chặn thẳng việc gán như vậy và báo lỗi ngay, coi như một lớp bảo vệ.
Alias trong dataclass có tự động ánh xạ tên khóa không?
Không. Thư viện chuẩn không có khái niệm alias, công cụ chỉ ghi tên khóa gốc vào phần metadata của trường như một quy ước để mã của bạn đọc lại. Bạn vẫn phải tự viết đoạn chuyển từ từ điển sang lớp. Muốn ánh xạ tự động thì phải dùng Pydantic.
Vì sao chọn TypedDict thì công tắc đổi tên khóa bị khóa lại?
Vì với TypedDict, khóa trong khai báo chính là khóa trong từ điển thật lúc chạy, không có lớp nào ở giữa để ánh xạ. Đổi tên trong khai báo nghĩa là khai báo sai so với dữ liệu. Do đó công cụ giữ nguyên khóa gốc và tắt công tắc đổi tên khi bạn chọn kiểu đầu ra này.
Khóa của tôi có dấu gạch nối thì TypedDict sinh ra thế nào?
Nó chuyển sang cú pháp hàm, tức khai báo bằng cách gọi TypedDict với một từ điển ánh xạ tên khóa sang kiểu, vì tên đó không phải định danh Python hợp lệ nên không viết được theo cú pháp lớp. Công cụ chèn thêm một dòng chú thích giải thích vì sao lớp đó khác các lớp còn lại.
NotRequired dùng để làm gì và cần phiên bản Python nào?
Nó đánh dấu một khóa có thể không có mặt trong từ điển, khác hẳn với việc khóa có mặt nhưng giá trị rỗng. Công cụ dùng nó cho những khóa chỉ xuất hiện ở một phần các phần tử của mảng. Cú pháp này có sẵn từ Python 3.11, phiên bản cũ hơn phải lấy từ gói mở rộng typing bên ngoài.
Trường mang giá trị rỗng trong mẫu ra kiểu gì?
Ra Optional của Any, nghĩa là công cụ thừa nhận không biết kiểu thật. Hãy thay bằng kiểu thật lấy từ tài liệu hoặc từ một mẫu khác có dữ liệu, giữ lại phần Optional nếu trường đó thật sự có thể rỗng. Để nguyên Any thì công cụ kiểm tra tĩnh không bắt được lỗi nào cho trường đó.
Lớp lồng có được đặt giá trị mặc định không?
Cố ý là không. Nếu đặt hàm nhà máy bằng chính tên lớp thì lúc chạy nó sẽ gọi lớp đó với danh sách tham số rỗng, và việc này vỡ ngay khi bên trong còn trường bắt buộc. Lớp lồng vì vậy luôn là trường bắt buộc, cũng đúng với thực tế là đối tượng con có trong dữ liệu thì phải có.
Cú pháp list ngoặc vuông thay cho List viết hoa cần Python bao nhiêu?
Từ Python 3.9 trở đi bạn dùng trực tiếp được list và dict viết thường kèm ngoặc vuông trong gợi ý kiểu. Với phiên bản cũ hơn, hãy thêm dòng nhập chế độ hoãn đánh giá chú thích ở đầu tệp, hoặc thay bằng List và Dict viết hoa lấy từ typing.
Model Pydantic sinh ra có nhận được cả tên khóa gốc lẫn tên snake_case không?
Có, vì công cụ thêm cấu hình cho phép khởi tạo bằng tên trường bên cạnh alias. Nhờ vậy bạn nạp được dữ liệu thật với tên khóa gốc, đồng thời viết mã kiểm thử bằng tên snake_case cho gọn. Cấu hình này chỉ được thêm khi có ít nhất một trường bị đổi tên.
Có phải dán JSON vào là mã sinh ra kiểm tra được dữ liệu ngay không?
Chỉ đúng với kiểu đầu ra Pydantic. Với dataclass và TypedDict, gợi ý kiểu chỉ phục vụ trình soạn thảo và công cụ kiểm tra tĩnh, còn lúc chạy thì không ai kiểm tra gì. Hãy kiểm định ngay tại ranh giới nhận dữ liệu rồi bên trong mới dùng cấu trúc đã sạch.
Từ khóa liên quan
- json to python dataclass
- chuyển json sang python class
- dataclass python là gì
- typeddict là gì
- pydantic v2 basemodel
- default_factory dataclass
- mutable default argument python
- optional type hint python
- type hint list str python
- alias trong pydantic
- populate_by_name pydantic
- đổi tên khóa sang snake_case
- notrequired typeddict
- thứ tự trường dataclass
- field metadata dataclass
- kiểm định dữ liệu api python
- parse json thành object python
- sinh model từ json python
- python type checking mypy
- công cụ python online miễn phí
