☰
marshmallow 实战示例指南:Schema 校验、REST API 与驼峰命名转换
2026/9/30 1:50:32 网站建设 项目流程
  • 后端
  • 序列化

【免费下载链接】marshmallow

A lightweight library for converting complex objects to and from simple Python datatypes.

项目地址:https://gitcode.com/gh_mirrors/ma/marshmallow
点击查看免费下载

本篇技术指南围绕 marshmallow 仓库中docs/examples/目录下的三个官方实战示例展开:package.json配置校验、Flask + SQLAlchemy 引语 REST API、以及基于on_bind_field钩子的驼峰键名自动转换。读完本文,你将掌握Schema.load校验与反序列化、自定义字段、data_key、unknown = INCLUDE、嵌套字段、dump_only、输出字段过滤、@pre_load预处理等核心能力的落地写法,并能在本地用uv一键复现全部示例。

示例概览与运行环境

docs/examples/下共有三个可独立运行的示例脚本,对应三篇独立文档:

示例文档脚本演示的核心特性
Validating package.jsonexamples/package_json_example.pySchema.load校验与反序列化、自定义字段、data_key、unknown = INCLUDE
Quotes APIexamples/flask_example.py自定义校验、嵌套字段、dump_only=True、only输出过滤、@pre_load预处理
Inflectionexamples/inflection_example.py通过Schema.on_bind_field钩子自动转换键名命名风格

所有示例均依赖 uv 运行。每个脚本都声明了 PEP 723 内联元数据(脚本头部的# /// script代码块),其中明确列出所需依赖与最低 Python 版本,uv会在首次运行时自动创建隔离环境并安装依赖,无需手动pip install。例如 examples/package_json_example.py 声明了marshmallow与packaging>=17.0,并要求requires-python = ">=3.10";examples/flask_example.py 则声明了flask、flask-sqlalchemy>=3.1.1、sqlalchemy>2.0和marshmallow。

示例一:用 Schema 校验 package.json 配置

需求场景与 Schema 设计

marshmallow 最常见的用途之一就是按 Schema 校验配置文件。第一个示例为package.json定义了一套校验 Schema,运行脚本后从stdin读取 JSON、校验并反序列化,输出规范化的 Python 数据结构。其完整脚本见 examples/package_json_example.py,Schema 定义如下:

from marshmallow import INCLUDE, Schema, ValidationError, fields class PackageSchema(Schema): name = fields.Str(required=True) version = Version(required=True) description = fields.Str(required=True) main = fields.Str(required=False) homepage = fields.URL(required=False) scripts = fields.Dict(keys=fields.Str(), values=fields.Str()) license = fields.Str(required=True) dependencies = fields.Dict(keys=fields.Str(), values=fields.Str(), required=False) dev_dependencies = fields.Dict( keys=fields.Str(), values=fields.Str(), required=False, data_key="devDependencies", ) class Meta: # Include unknown fields in the deserialized output unknown = INCLUDE

该 Schema 展示了多个重要特性:

  • 校验与反序列化一体:Schema.load(底层实现在 src/marshmallow/schema.py)在校验通过后返回已反序列化的数据,而非原样字符串。例如version字段会被转换为packaging.version.Version对象。
  • data_key指定反序列化键名:JSON 中 npm 生态习惯使用驼峰的devDependencies,而 Python 端字段名是蛇形的dev_dependencies。通过data_key="devDependencies",Schema 会从输入数据的devDependencies键取值。从源码看,无论是校验(schema.py、schema.py)还是序列化输出(schema.py),data_key均优先于字段名决定外部键名;且dump时若多个字段的data_key或名称互相冲突会抛出明确异常。
  • unknown = INCLUDE保留未知键:package.json中通常含有大量 Schema 未声明的字段(如keywords、author)。在Meta中设置unknown = INCLUDE后,这些未知键不会被丢弃,而是原样并入反序列化结果,适合"只要校验关键字段、其余字段照单全收"的配置校验场景。marshmallow 还提供EXCLUDE(默认,丢弃未知键)与RAISE(遇到未知键抛ValidationError)两种策略。
  • Dict字段与嵌套键值类型约束:scripts、dependencies等对象被建模为fields.Dict,并同时约束键与值的类型,例如keys=fields.Str(), values=fields.Str()强制"字符串键 + 字符串值"。

自定义 Version 字段

package.json的version使用 semver 语义化版本号。示例并未使用现成的fields.String,而是基于packaging.version实现了自定义字段Version,其中fields.Field[version.Version]是该字段的泛型标注,表明它反序列化产出Version对象:

class Version(fields.Field[version.Version]): """Version field that deserializes to a Version object.""" def _deserialize(self, value, *args, **kwargs): try: return version.Version(value) except version.InvalidVersion as e: raise ValidationError("Not a valid version.") from e def _serialize(self, value, *args, **kwargs): return str(value)
  • _deserialize:在load阶段被调用(对应 src/marshmallow/fields.py 中Field基类_deserialize方法体系),把字符串转为Version对象;解析失败时抛出带中文上下文的ValidationError("Not a valid version."),该错误信息最终会以字典形式出现在error.messages中。
  • _serialize:在dump阶段把Version对象转回字符串,保证序列化方向仍然输出 JSON 友好的数据。
  • 自定义字段是 marshmallow 扩展体系的核心能力之一,更系统的说明见 custom_fields.rst。

运行方式与两种输入输出

脚本主体从sys.stdin读取 JSON,调用PackageSchema().load(pkg),校验失败则打印错误并退出码 1:

if __name__ == "__main__": pkg = json.load(sys.stdin) try: pprint(PackageSchema().load(pkg)) except ValidationError as error: print("ERROR: package.json is invalid") pprint(error.messages) sys.exit(1)

合法输入(examples/package.json):

{ "name": "dunderscore", "version": "1.2.3", "description": "The Pythonic JavaScript toolkit", "devDependencies": { "pest": "^23.4.1" }, "main": "index.js", "scripts": { "test": "pest" }, "license": "MIT" }

执行命令与输出:

$ uv run examples/package_json_example.py < examples/package.json {'description': 'The Pythonic JavaScript toolkit', 'dev_dependencies': {'pest': '^23.4.1'}, 'license': 'MIT', 'main': 'index.js', 'name': 'dunderscore', 'scripts': {'test': 'pest'}, 'version': <Version('1.2.3')>}

注意两点:devDependencies被映射为字段名dev_dependencies;version被自定义字段反序列化成了<Version('1.2.3')>对象,这正是自定义字段的价值所在。

非法输入(examples/invalid_package.json):

{ "name": "dunderscore", "version": "INVALID", "homepage": "INVALID", "description": "The Pythonic JavaScript toolkit", "license": "MIT" }

version不是合法语义化版本、homepage不是合法 URL,执行结果:

$ uv run examples/package_json_example.py < examples/invalid_package.json ERROR: package.json is invalid {'homepage': ['Not a valid URL.'], 'version': ['Not a valid version.']}

fields.URL内置校验与自定义字段的校验同时生效,错误信息按字段名聚合成字典,非常便于程序化处理和用户提示。

示例二:Flask + SQLAlchemy 引语 REST API

项目结构:模型、Schema 与路由

第二个示例是一个完整可运行的引语(Quotes)REST API,完整代码见 examples/flask_example.py。它演示了 marshmallow 与 Web 框架、ORM 的典型协作模式:Schema 负责输入校验与输出序列化,SQLAlchemy 模型负责持久化。关键设计如下:

  • 数据模型:Author(id、first、last)与Quote(id、content、author_id、posted_at),Quote.author通过relationship关联Author,并带backref("quotes", lazy="dynamic");数据库使用 SQLite 文件sqlite:////tmp/quotes.db,启动时通过db.create_all()建表。
  • Schema 实例化策略:文件底部为每个 Schema 预建了单例,并用many=True派生集合 Schema:
author_schema = AuthorSchema() authors_schema = AuthorSchema(many=True) quote_schema = QuoteSchema() quotes_schema = QuoteSchema(many=True, only=("id", "content"))

四个核心特性逐一拆解

1.dump_only=True声明只读字段

class AuthorSchema(Schema): id = fields.Int(dump_only=True) first = fields.Str() last = fields.Str() formatted_name = fields.Method("format_name", dump_only=True)

id、formatted_name和QuoteSchema中的posted_at都标记为dump_only:这些字段只在dump(序列化输出)时出现,load时被忽略且不参与校验——正好对应数据库自增主键、服务端计算字段、服务端写入时间戳这三类"客户端不该提供"的数据。fields.Method("format_name", ...)则是序列化钩子,dump时调用format_name(author)方法生成"Peters, Tim"形式的全名。

2. 自定义校验函数

def must_not_be_blank(data): if not data: raise ValidationError("Data not provided.") class QuoteSchema(Schema): id = fields.Int(dump_only=True) author = fields.Nested(AuthorSchema, validate=must_not_be_blank) content = fields.Str(required=True, validate=must_not_be_blank) posted_at = fields.DateTime(dump_only=True)

validate参数接受一个可调用对象,返回值不为真即视为校验失败并抛ValidationError。这里对嵌套的author与必填的content都附加了"非空白"校验,避免空字符串入库。

3. 嵌套字段与校验错误合并

fields.Nested(AuthorSchema)让引语对象在输入时嵌入完整的作者对象。当客户端 POST 时省略author,must_not_be_blank会触发,错误以{"author": ["Data not provided."]}形式返回(实际返回 HTTP 422,见下文new_quote路由的异常处理)。嵌套字段的详细用法见 nesting.rst。

4.only参数过滤输出字段

QuoteSchema(many=True, only=("id", "content"))在序列化列表时只输出id和content,隐藏author、posted_at等字段。这正是"引语列表只需要展示内容、无需重复回显作者与时间戳"的典型场景。

@pre_load:请求体预处理

QuoteSchema定义了一个关键的@pre_load钩子(对应 src/marshmallow/decorators.py 的pre_load装饰器,完整语义见 pre_and_post_processing.rst):

# Allow client to pass author's full name in request body # e.g. {"author': 'Tim Peters"} rather than {"first": "Tim", "last": "Peters"} @pre_load def process_author(self, data, **kwargs): author_name = data.get("author") if author_name: first, last = author_name.split(" ") author_dict = {"first": first, "last": last} else: author_dict = {} data["author"] = author_dict return data

它允许客户端直接提交{"author": "Tim Peters", "content": "..."}这种扁平结构,在真正进入字段级反序列化之前,把作者全名拆成{"first": "Tim", "last": "Peters"}字典。若作者不存在,则置为空字典以触发后续的must_not_be_blank校验。@pre_load处理发生在Schema.load内部、字段校验之前(对应 schema.py 中 load 流程对pre_load处理器的调用点),因此转换对调用方完全透明。

路由与错误处理

API 提供了 5 个端点:

  • GET /authors:序列化全部作者(authors_schema.dump(authors))。
  • GET /authors/<int:pk>:查单作者,返回作者对象与他的全部引语(quotes_schema.dump(author.quotes.all()));查无此人返回 400。
  • GET /quotes/:返回全部引语的精简列表(只含id、content)。
  • GET /quotes/<int:pk>:查单条引语。
  • POST /quotes/:接收 JSON,quote_schema.load(json_data)校验反序列化;校验失败时把err.messages原样返回并给 422 状态码;成功则复用或新建作者、写入引语并提交事务。
@dataclass @app.route("/quotes/", methods=["POST"]) def new_quote(): json_data = request.get_json() if not json_data: return {"message": "No input data provided"}, 400 try: data = quote_schema.load(json_data) except ValidationError as err: return err.messages, 422 ...

注意这里quote_schema.load返回的数据已经经过@pre_load拆分与嵌套反序列化,data["author"]是含first/last的字典,可直接驱动 ORM 查询或建库。另外注意示例文档中的校验失败响应示例返回了字段错误字典({"author": ["Data not provided."]}),而源码实际返回 422 状态码。

动手运行 API

依次执行:

$ uv run examples/flask_example.py

启动后服务监听 5000 端口。官方示例使用 httpie 发送请求,可先用 uv 安装:

$ uv tool install httpie

POST 几条引语:

$ http POST :5000/quotes/ author="Tim Peters" content="Beautiful is better than ugly." $ http POST :5000/quotes/ author="Tim Peters" content="Now is better than never." $ http POST :5000/quotes/ author="Peter Hintjens" content="Simplicity is always better than functionality."

校验失败时的响应(故意省略 author):

$ http POST :5000/quotes/ content="I have no author" { "author": [ "Data not provided." ] }

GET 全部引语(注意only=("id", "content")生效,无 author 与时间戳):

$ http :5000/quotes/ { "quotes": [ { "content": "Beautiful is better than ugly.", "id": 1 }, { "content": "Now is better than never.", "id": 2 }, { "content": "Simplicity is always better than functionality.", "id": 3 } ] }

GET 某位作者及其引语:

$ http :5000/authors/1 { "author": { "first": "Tim", "formatted_name": "Peters, Tim", "id": 1, "last": "Peters" }, "quotes": [ { "content": "Beautiful is better than ugly.", "id": 1 }, { "content": "Now is better than never.", "id": 2 } ] }

从响应可见dump_only的formatted_name由fields.Method动态生成,作者列表的引语同样被only过滤为id+content。这个示例完整覆盖了 marshmallow 在真实 Web 服务中最常见的全部配合模式,也是理解 quickstart.rst 之后的最佳进阶素材。

示例三:用 on_bind_field 实现键名自动变形(Inflection)

问题:HTTP API 的驼峰键名

很多 HTTP API 的对外 JSON 使用驼峰键(firstName),而 Python 代码习惯蛇形命名(first_name)。逐个给字段写data_key既繁琐又易漏。第三个示例(examples/inflection_example.py)演示了如何用一个基类彻底解决该问题。

核心:重写 on_bind_field 钩子

Schema.on_bind_field在字段绑定到 Schema 时被调用一次,接收字段名与字段对象(定义见 schema.py)。利用这个时机修改字段的data_key,即可在加载/序列化两个方向上统一键名转换:

from marshmallow import Schema, fields def camelcase(s): parts = iter(s.split("_")) return next(parts) + "".join(i.title() for i in parts) class CamelCaseSchema(Schema): """Schema that uses camel-case for its external representation and snake-case for its internal representation. """ def on_bind_field(self, field_name, field_obj): field_obj.data_key = camelcase(field_obj.data_key or field_name)
  • camelcase("first_name")输出firstName:首个下划线片段保留原样,后续片段首字母大写后拼接。
  • field_obj.data_key or field_name保证:若字段已显式指定data_key,优先保留显式值;否则用字段名做转换。
  • 由于on_bind_field在 Schema 声明期统一执行,后续所有继承CamelCaseSchema的子类自动获得驼峰外部表示,无需重复编码。

使用与运行结果

class UserSchema(CamelCaseSchema): first_name = fields.Str(required=True) last_name = fields.Str(required=True) schema = UserSchema() loaded = schema.load({"firstName": "David", "lastName": "Bowie"}) print("Loaded data:") print(loaded) dumped = schema.dump(loaded) print("Dumped data:") print(dumped)

运行:

$ uv run examples/inflection_example.py Loaded data: {'first_name': 'David', 'last_name': 'Bowie'} Dumped data: {'firstName': 'David', 'lastName': 'Bowie'}

load把外部的驼峰键转换为 Python 侧蛇形键,dump再把蛇形键还原为 API 要求的驼峰键——一套 Schema 同时服务两个方向。若还需要支持复数化、kebab-case 等更复杂的变形规则,可借助第三方库(如 inflection)在camelcase函数内实现,而不必改动 Schema 框架本身。

从示例到源码:三个特性的底层原理

三个示例用到的核心机制在源码中都有明确对应:

  • Schema.load的校验与反序列化流程:加载过程中先应用pre_load等处理钩子,再按字段逐一调用_deserialize完成类型转换与校验,错误统一汇集到error_store,最终以字段名/data_key为键聚合到ValidationError.messages(可对照 src/marshmallow/schema.py 的 load 实现与 src/marshmallow/error_store.py)。
  • data_key的作用点:Schema 的序列化与校验路径中均以field_obj.data_key if field_obj.data_key is not None else field_name决定外部键名(schema.py、schema.py、schema.py),同时dump时会检测重复的data_key并抛错。这正是示例一devDependencies映射与示例三on_bind_field修改data_key得以生效的根本原因。
  • 自定义字段的_deserialize/_serialize契约:Field基类(src/marshmallow/fields.py)通过_deserialize与_serialize两个钩子定义双向转换,内置的Str、URL、Dict、DateTime、Int等类型均遵循同一契约,示例一的Version字段正是对这一扩展点的直接利用。

这三个示例也分别对应文档站上的独立章节:validating_package_json.rst、quotes_api.rst 与 inflection.rst,可与 custom_fields.rst、nesting.rst、pre_and_post_processing.rst 等参考文档配合阅读,形成完整的知识闭环。

  • 后端
  • 序列化

【免费下载链接】marshmallow

A lightweight library for converting complex objects to and from simple Python datatypes.

项目地址:https://gitcode.com/gh_mirrors/ma/marshmallow
点击查看免费下载
上一篇:3D ViewPager:让你的Android应用拥有惊艳立体翻页效果
下一篇:floating-ui尺寸调整:根据内容动态调整浮动元素大小

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询