
Hasura v3opendds-derive派生宏完全指南为 OpenDd 类型自动实现 JSON 反序列化与 JSON Schema 生成【免费下载链接】graphql-engineBlazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.项目地址: https://gitcode.com/gh_mirrors/gr/graphql-engineopendds-derive是 Hasura v3 引擎graphql-engine 仓库v3/目录中的一个 Rust 过程宏 crate它为open-ddscrate 中定义的OpenDdtrait 提供#[derive(OpenDd)]派生支持只需在结构体或枚举上声明属性即可自动获得从 JSON 值反序列化 OpenDDOpen Data Domain元数据、并生成配套 JSON Schema 的能力。本文将以 v3/crates/utils/opendds-derive/README.md 为骨架结合 lib.rs、struct_derive.rs、enum_derive.rs、container.rs 等源码与 open-dds 中的真实用法完整讲解其 trait 契约、属性语法、五种枚举标记策略与实战要点。读完本文你将能独立为自己的元数据类型接入这套派生宏体系并理解 Hasura v3 元数据解析与 JSON Schema 生成的底层原理。1. 背景OpenDD 与OpenDdtraitHasura v3 引擎使用 OpenDDOpen Data Domain开放数据域来描述引擎运行所需的全部元数据——数据连接器、模型、命令、权限、视图等对象统一以 JSON 形式表达见 open-dds/src/lib.rs 中Metadata类型的注释 “All of the metadata required to run Hasura v3 engine”。由于这些元数据需要从用户提交的 JSON 严格反序列化为强类型 Rust 结构反序列化失败时给出精确到 JSON 路径如$.subgraphs[1].kind的错误定位为每种类型生成可供下游工具消费的 JSON Schema。open-ddscrate 定义了OpenDdtrait 来统一承载这些能力。而opendds-derive则负责为任意自定义类型自动生成该 trait 的实现避免手写大量样板代码。2.OpenDdtrait核心契约2.1 定义以当前仓库源码为准README 中给出了 trait 的概要形式。需要特别说明的是当前仓库中的实际签名已在deserialize方法上增加了path: jsonpath::JSONPath参数用于在递归反序列化时传递并累积错误路径见 v3/crates/open-dds/src/traits.rspub trait OpenDd: Sized { fn deserialize( json: serde_json::Value, path: jsonpath::JSONPath, ) - ResultSelf, OpenDdDeserializeError; fn json_schema(generator: mut schemars::r#gen::SchemaGenerator) - schemars::schema::Schema; fn _schema_name() - String; fn _schema_is_referenceable() - bool { false } }四个方法各司其职方法职责deserialize从serde_json::Value反序列化类型path参数用于记录错误发生的 JSON 路径json_schema借助schemars的SchemaGenerator生成该类型的 JSON Schema_schema_name返回 Schema 名称用于$ref引用与definitions去重_schema_is_referenceable该类型是否可被$ref引用派生宏生成实现时默认返回true2.2 为什么要绕过 serde 另起炉灶traits.rs 的文档注释明确解释了原因单纯使用serde::de::Deserialize配合serde_path_to_error对于untagged无标签和internally tagged内部标签枚举无法得到正确的错误路径——这是 serde 的已知局限源码注释引用了 serde 仓库的 issue #1183 与 #1495。OpenDdtrait 正是针对该局限的 workaround反序列化逻辑由派生宏生成代码逐层手工驱动每一层都显式维护JSONPath。2.3 内建类型实现不是所有类型都需要派生宏。open-dds通过宏批量提供了基础类型与容器类型的实现见 v3/crates/open-dds/src/traits/macros.rs 与 traits.rs标量/叶子类型String、SmolStr、bool、i32、u32、u64、()、serde_json::Value通过impl_OpenDd_default_for!宏直接复用 serde 的from_value注释说明叶子类型错误路径恒为空无需serde_path_to_error的开销。包装类型BoxT、Cowstatic, T、OptionTOption对Null返回None其余委托给T。序列容器VecT、IndexSetT、HashSetT、BTreeSetT通过seq_impl!宏实现反序列化时逐元素调用deserialize_index记录数组下标。映射容器IndexMap、HashMap、BTreeMap通过map_impl!宏实现。因此当你的结构体字段由这些类型组合而成时只需对顶层类型使用派生宏即可。3. 快速开始接入依赖并启用派生3.1 添加依赖在Cargo.toml中同时加入opendds-derive与open-dds[dependencies] open-dds { path ... } opendds-derive { path ... }opendds-derive自身的依赖与配置可参考 v3/crates/utils/opendds-derive/Cargo.toml它是proc-macro true的过程宏 crate内部基于darling、syn、quote、proc-macro2、convert_case用于 camel-case 转换与regex构建。3.2 使用派生宏use opendds_derive::OpenDd; #[derive(OpenDd)] struct NamedFieldStruct { named_field_1: Type1, named_field_2: Type2, }目前仅支持struct与enum两种类型的派生lib.rs中Data::Union会直接报错 “only enums and structs are supported”见 lib.rs。所有控制属性统一写在#[opendd(...)]中用于类型级、字段级或变体级配置。3.3 前置 crate 可见性要求README “Notes” 一节强调使用宏的模块中必须能访问以下路径否则生成代码无法编译open_dds宏生成的代码以open_dds::traits::OpenDd、open_dds::traits::OpenDdDeserializeError等路径引用 trait 与错误类型注意 crate 名是下划线形式open_ddsstrumuntagged_with_kind等策略需要strum::VariantNames来枚举内部枚举的所有变体名。4. Struct 派生详解4.1 支持范围与限制container.rs 中的StructData::from_fields决定了支持边界支持命名字段结构体named fields与单字段元组结构体newtype即恰好一个未命名字段不支持多字段元组结构体如struct UnnamedFieldStruct(Type1, Type2)与单元结构体如struct UnitStruct;编译期直接报错。newtype 的反序列化实现会调用open_dds::traits::deserialize_index(json, path, 0)其 JSON Schema 直接委托给内部字段类型见 struct_derive.rs。4.2 公共行为对所有命名字段结构体派生宏保证两条统一语义README “Common Behavior”字段按 camel-case 反序列化Rust 字段名named_field_1对应 JSON 键namedField1。默认命名转换逻辑见 container.rsto_case(Case::Camel)出现未知字段即报错反序列化过程中宏生成的代码会从 JSON 对象中逐一remove已知键若最终还有剩余键则报unexpected keys: ...; expecting: ...见 struct_derive.rs。对应测试用例见 traits.rs。同时生成的 JSON Schema 会设置additionalProperties: false并计算required字段列表无default且非Option的字段必填见 struct_derive.rs。4.3 字段级属性Field Level下表汇总 README 列出的全部字段级属性及其源码对应属性作用源码依据#[opendd(default)]字段缺失时使用Default::default()DefaultAttribute::Flagcontainer.rs代码生成见 struct_derive.rs#[opendd(default value)]字段缺失时使用给定的表达式值DefaultAttribute::Expr支持字符串字面量与整数字面量#[opendd(rename name)]用给定名称代替 camel-case 字段名进行反序列化FieldOpts.renamecontainer.rs#[opendd(hidden true)]从 JSON Schema 中隐藏该字段用于隐藏未完成的工作FieldOpts.hidden注意源码约束hidden 字段必须是OptionT或带default否则编译报错见 container.rs#[opendd(deserialize_with function_name)]使用自定义反序列化函数处理该字段struct_derive.rs#[opendd(json_schema(default_exp some::function()))]为 schema 的default提供自定义 JSON 值JsonSchemaFieldOpts.default_expcontainer.rs#[opendd(json_schema(title title string value))]设置生成 schema 中该字段的titleJsonSchemaFieldOpts.titledeserialize_with的函数签名必须是fn(serde_json::Value, jsonpath::JSONPath) - ResultT, OpenDdDeserializeError其中T为该字段的类型。open-dds中的测试用例展示了一个给字符串加前缀的自定义反序列化器见 traits.rs。json_schema(default_exp)的使用前提它必须与#[opendd(default)]配合使用。如果字段类型实现了serde::Serialize则不需要显式提供——宏会自动用serde_json::json!(Default::default())推断默认 JSON 值否则需要通过该属性显式给出返回 JSON 值的函数表达式见 struct_derive.rs。5. Enum 派生详解五种标记策略5.1 统一限制所有枚举变体必须恰好携带一个未命名字段即每个变体是一个元组变体仅一个字段见 container.rs// 支持 #[derive(opendds_derive::OpenDd)] enum MyEnum { VariantOne(TypeOne), VariantTwo(TypeTwo), } // 不支持无字段、多字段、命名字段均会编译报错 enum Unsupported { NoFields, MultipleUnnamed(TypeOne, TypeTwo), Named{ field_1: TypeOne, field_2: TypeTwo }, }枚举从 JSON 对象反序列化时依赖一个标签键值对tag来确定具体变体。类型级属性决定标签的键名、取值匹配规则与变体内容的取法。源码中EnumImplStyle枚举统一了这五种风格见 container.rs每种风格对应的标签与内容键定义在EnumTagType见 enum_derive.rs。5.2as_versioned_internally_tagged使用version键作为标签标签值与变体名的camel-case形式匹配如变体V1匹配v1对象其余内容直接作为变体值反序列化。#[derive(opendds_derive::OpenDd)] #[opendd(as_versioned_internally_tagged)] enum VersionedEnum { V1(VersionOne), V2(VersionTwo), }以下 JSON 被解析为V1(VersionOne){ version: v1, fieldOne: some_value }在 open-dds/src/lib.rs 中MetadataWithVersion正是采用此策略的实战案例变体V1/V2/V3分别匹配 JSON 中的version: v1等值。5.3as_versioned_with_definition同样使用version键作为标签标签值匹配变体名的 camel-case 形式但变体内容取自对象中definition键的值。#[derive(opendds_derive::OpenDd)] #[opendd(as_versioned_with_definition)] enum VersionedEnum { V1(VersionOne), V2(VersionTwo), }以下 JSON 被解析为V2(VersionTwo){ version: v2, definition: { fieldTwo: some_value } }这种 “版本号 独立定义块” 的形态在 open-dds 的元数据模型中被广泛采用例如SuperGraph枚举见 open-dds/src/lib.rs。源码中它对应EnumTagType::Adjacent { tag: version, content: definition }反序列化时会先从对象中remove(definition)缺失则报missing field错误enum_derive.rs。5.4as_kind使用kind键作为标签标签值与变体名做精确匹配不做大小写转换见 container.rs 中Tagged::KindInternal variant_name.to_string()对象其余内容直接作为变体值反序列化。#[derive(opendds_derive::OpenDd)] #[opendd(as_kind)] enum KindEnum { KindOne(KindOneStruct), KindTwo(KindTwoStruct), }以下 JSON 被解析为KindOne(KindOneStruct){ kind: KindOne, fieldOne: 111, fieldTwo: false, fieldThree: three }这是 Hasura v3 元数据中最常用的策略OpenDdSubgraphObject枚举数据连接器、模型、命令、权限等对象的统一容器与OpenDdSupergraphObject均以#[opendd(as_kind)]派生见 open-dds/src/lib.rs 与 L297-L308。5.5untagged_with_kindJSON 对象不直接携带变体标签每个变体必须指向一个本身实现了#[opendd(as_kind)]的内部枚举通过内部枚举的kind值借助strum_macros::VariantNames的VARIANTS列表逐层匹配先确定外层变体再还原kind键驱动内部枚举反序列化。#[derive(opendds_derive::OpenDd)] #[opendd(as_kind)] enum KindEnumOne { VariantOne(OneStruct), VariantTwo(TwoStruct) } #[derive(opendds_derive::OpenDd)] #[opendd(as_kind)] enum KindEnumTwo { VariantThree(ThreeStruct), VariantFour(FourStruct) } #[derive(opendds_derive::OpenDd)] #[opendd(untagged_with_kind)] enum UntaggedEnum { KindOne(KindEnumOne), KindTwo(KindEnumTwo), }以下 JSON 被解析为UntaggedEnum::KindTwo(KindEnumTwo::VariantFour(FourStruct { field_four: four })){ kind: VariantFour, fieldFour: four }源码实现要点enum_derive.rs宏生成一串if #ty::VARIANTS.contains(__tag_value_str)条件分支命中后将kind键重新插回对象再委托内部枚举的OpenDd::deserialize若全部未命中则汇总所有内部枚举的变体名并报unexpected value: ... expecting ...错误路径指向$.kind。对应测试见 traits.rs。5.6externally_tagged外部标签JSON 对象恰好包含一个键键名即变体名camel-case键值即变体内容。#[derive(opendds_derive::OpenDd)] #[opendd(externally_tagged)] enum ExternallyTaggedEnum { VariantOne(VariantOneStruct), VariantTwo(VariantTwoStruct), }以下 JSON 被解析为ExternallyTaggedEnum::VariantTwo(VariantTwoStruct { prop_1: true, prop_2: testing }){ variantTwo: { prop1: true, prop2: testing } }源码实现会取对象的第一个键作为标签值并严格校验空对象报found empty object、含多个属性报found multiple object properties、未知键报unknown variant且错误路径为$见 enum_derive.rs 与 traits.rs 的多个边界测试。5.7 变体级属性Variant Level属性作用#[opendd(rename name)]用给定名称反序列化该变体并用作 schema 中 tag 的枚举值#[opendd(alias name)]允许从该别名或由 Rust 变体名推导出的名称反序列化对应生成代码中的#variant_name_str | #alias_str匹配分支见 enum_derive.rs#[opendd(hidden true)]从 JSON Schema 中隐藏该变体注意hidden变体仍然可以反序列化仅不进入 schema见 traits.rs 的专门测试#[opendd(json_schema(title ...))]设置生成 schema 的title仅对as_versioned_with_definition与externally_tagged生效#[opendd(json_schema(example some::function))]将函数返回值写入 schema 的examples仅对as_versioned_with_definition生效6. 通用 JSON Schema 属性类型级以下属性对 struct 与 enum 均适用写在#[opendd(json_schema(...))]中属性作用rename生成的 schema 使用给定名称替代 Rust 类型名title设置 schema 的titleid设置 schema 的$idJSON Schema 草案的标识符见 draft-handrews-json-schema-02 §8.2.2example将指定函数的返回值写入 schema 的examples可重复声明多次源码中以Vecsyn::Path收集见 container.rs6.1 元数据生成规则源码视角container.rs 中Container::from_derive_input实现了以下推导规则Schema ID由id属性决定回退到rename再回退到 Rust 类型名最终统一加上前缀https://hasura.io/jsonschemas/metadata/Schema 名称由rename决定回退到id自动从 ID 生成名称时会把空格、斜杠等可能破坏代码生成器的字符替换为_正则[^0-9A-Za-z_]Schema title由title决定回退到doc 注释见下再回退到 schema 名称。6.2 doc 注释自动提取宏会自动从///doc 注释中提取 title 与 descriptionhelpers.rs注释以#开头时第一行作为title其余作为description否则整段注释作为description段落间以\n\n分隔。生成 schema 时apply_schema_metadata会将$id、title、description、examples一并写入helpers.rs。此外traits.rs 中的gen_root_schema_for还会为所有已设置 title 的定义补充$id并对 definitions 执行去重jsonschema_tidying::deduplicate_definitions。6.3 schema 校验器形态对内部标签枚举as_kind、as_versioned_internally_tagged每个变体子 schema 的properties中会被插入常量kind/version属性enum: [VariantName]并移动到首位、加入required见 enum_derive.rs当所有变体标签名互不重复时schema 使用oneOf否则降级为anyOfvariant_subschemashelpers.rs。7. 实战案例open-dds 中的真实用法结合 open-dds/src/lib.rs 与测试代码可以看到派生宏在真实元数据模型中的组合运用。7.1 版本化元数据MetadataWithVersion#[derive(Serialize, Clone, Debug, PartialEq, opendds_derive::OpenDd)] #[serde(tag version, rename_all camelCase)] #[opendd( as_versioned_internally_tagged, json_schema(rename OpenDdMetadataWithVersion) )] pub enum MetadataWithVersion { #[serde(alias V1)] #[opendd(alias V1)] V1(MetadataV1), // ... V2, V3 }它同时组合了as_versioned_internally_tagged反序列化与json_schema(rename ...)schema 命名并通过变体级alias让v1与V1都能命中V1变体。整个Metadata类型则采用手写的 untagged 分发数组 → 无命名空间列表对象 → 版本化元数据见 open-dds/src/lib.rs。7.2 默认值flags字段MetadataV1的flags字段展示了default与json_schema(default_exp)的搭配#[opendd( default, json_schema(default_exp serde_json::to_value(flags::OpenDdFlags::default()).unwrap()) )] pub flags: flags::OpenDdFlags,反序列化时flags缺失则取OpenDdFlags::default()JSON Schema 中则用default_exp显式给出默认值的 JSON 表达OpenDdFlags未直接实现serde::Serialize时的必要补充。类似的还有MetadataV2/MetadataV3中的supergraph/subgraphs默认值Supergraph::default_json()与serde_json::json!([])见 open-dds/src/lib.rs。7.3 测试验证traits.rs 中内置了大量测试是理解行为边界的绝佳素材test_parse_struct嵌套 versioned 枚举 kind 枚举 Option字段的完整解析L538-L583test_struct_parse_error_path验证错误路径精确到$.subgraphs[1].kindL616-L646test_json_schema_structs/test_json_schema_enum_with_hidden_item校验生成的完整 JSON Schema 结构包括$id、title、description、examples、oneOf、additionalProperties: false等L666-L844。另外仓库根目录的 v3/crates/open-dds/examples/reference.json 是Metadata类型的参考元数据样本test_serialize_reference_metadataopen-dds/src/lib.rs会对它做 “反序列化 → 序列化 → 再反序列化” 的往返一致性校验。8. 注意事项与最佳实践前置模块可见性使用宏的模块必须能访问open_dds宏生成代码以open_dds::traits::*引用与strumuntagged_with_kind依赖strum::VariantNames否则编译失败。struct 形态受限只支持命名字段与 newtype元组多字段、单元结构体请先重构。enum 变体必须单字段所有变体必须是恰好一个未命名字段若确实需要多字段可将字段聚合为一个结构体。hidden的约束struct 字段若要hidden true必须是OptionT或带有defaultenum 变体hidden true仅影响 schema不影响反序列化——刻意为之用于“解析仍接受、但不出现在公共 API schema 中”的过渡场景。未知字段是错误默认语义是拒绝未知键additionalProperties: false同步反映在 schema 中这保证了元数据的严格性但也意味着上游新增字段会破坏解析属预期行为。错误路径体验OpenDdDeserializeError携带serde_json::Error与JSONPath两部分traits.rs展示错误时优先呈现path这是本宏相对 serde 方案的核心收益。9. 总结opendds-derive通过一个派生宏把 OpenDD 元数据体系的三个核心诉求——强类型 JSON 反序列化、精确的错误路径、可消费的 JSON Schema——统一收敛到声明式属性上。理解其 struct/enum 两种形态、六类字段级与变体级属性、五种枚举标记策略as_versioned_internally_tagged、as_versioned_with_definition、as_kind、untagged_with_kind、externally_tagged以及json_schema(...)元数据规则即可在为 Hasura v3 扩展自定义元数据类型时做到举一反三。若想深入源码建议从 v3/crates/utils/opendds-derive/src/container.rs属性解析与 v3/crates/open-dds/src/traits.rstrait 与测试两个文件入手对照本文各节的源码索引逐一印证。【免费下载链接】graphql-engineBlazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.项目地址: https://gitcode.com/gh_mirrors/gr/graphql-engine创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考