#json-schema #generate #parser-generator #code-generation #document

schema2code

从符合 JSON Schema 规范的文档中生成任何语言的代码

5 个版本

0.1.4 2023年9月25日
0.1.3 2023年9月25日
0.1.2 2023年9月25日
0.1.1 2023年9月3日
0.1.0 2023年9月3日

#391 in 开发工具

MIT/Apache

41KB
799

Schema 解析器 & 代码生成器

一个不依赖于特定语言的模型生成器,用于 JSON schemas 的一个子集。(目前仅限 Rust,下一个计划:python + pydantic)

如何使用

查看 codegen-test 包以了解如何将其用于编译时生成代码。

Schema 约束

在编写 parserdeserializer 时,我添加了一些可能的 schemas 的约束,这仅允许 asyncapi schema 定义的一个子集。

  • 文档中的 components -> schemas 部分的每个顶级项都必须是实际的 schema,这不被允许
components:
  schemas:
    MyEntity:
      $ref: '#/components/schemas/AnotherEntity'
  • 每个顶级 schema 只能是以下之一:[AllOf, OneOf, AnyOf, type: object],顶级数组类型目前不可用,在您的 asyncapi schema 中,我建议在规范的 messages 部分创建匿名 schema,并在 components/schemas 部分创建特定的 item 类型,以便您的代码具有项目类型,并且可以通过包装在特定语言集合中来轻松反序列化有效负载。
  • 当前枚举仅支持 String 值,即使在生成时的反序列化/解析中支持数值枚举,也会抛出错误,因为我尚未创建特定类型来区分它们和文字枚举。
  • 每次指定 const 值时,都必须指定一个 type
  • 目前仅支持整数,任何 format 指令都简单地被忽略
  • 由于当前 AllOf 的实现方式,重复属性会在 Rust 中引起错误,当前的代码生成器只是将合并后的模式取出来,为每个(如果设置了 title 则为命名一个)创建一个 AnonymousEntity,然后通过在结构体中使用 #[serde(flatten)] 来合并它们,如果合并后的模式定义了重叠属性,这将导致反序列化失败。(这个问题已经在我的计划中,但不是优先考虑的事项,在面向对象的语言中,我的代码生成器会简单地扩展所有 AllOf 模式类,重复属性将由编程语言的继承来处理)

针对 Rust 用户的说明

  • 对于设置了特定 discriminatorOneOf 模式,它目前只有在判别器匹配实体的名称时才起作用(对于匿名实体,为确定性的名称设置 title 属性),否则,如果您在 const 字段中使用特殊值作为判别器,目前需要暂时省略 discriminator 并仅使用生成的 #[serde(untagged)] 枚举,使用 const 字段将使用 monostate 包来尊重。
  • 当前 AllOf 模式不合并属性,出于简便性,它为内部模式创建结构体,然后将它们通过 #[serde(flatten)] 合并到单个结构体中。(出于简便性,我可能会在其他语言中使用类似的解决方案,即有一个命名的空类从匿名/命名的结构体继承其字段)

计划

  • 用于代码生成的 CLI 工具
  • Python pydantic 模型生成器
  • Protobuf 生成器

问题

反序列化器定义了用于模式反序列化的 untagged 枚举,其中包含 monostate::MustBe,当模式不匹配时,会导致相当无用的错误消息,大多数错误都是 Did not match any variant in SchemaDef

示例

Asyncapi 模式定义

RequestBase:
  type: object
  additionalProperties:
    type: array
  properties:
    id:
      type: string
      description: "correlation id to match request and response"
    kind:
      type: string
      const: request
    tupleProp:
      type: array
      items: false
      prefixItems:
       - type: string
       - type: object
  required:
    - id

GetUser:
  description: TODO
  allOf:
  - $ref: '#/components/schemas/RequestBase'
  - type: object
    title: GetUserInner
    properties:
      data:
        title: GetUserData
        type: object
        properties:
          userId:
            type: string
          name:
            type: string
        required:
          - userId
    required:
      - data
      - event

DeleteUser:
  description: TODO
  allOf:
  - $ref: '#/components/schemas/RequestBase'
  - type: object
    title: DeleteUserInner
    properties:
      data:
        title: DeleteUserData
        type: object
        properties:
          userId:
            type: string
        required:
          - userId
    required:
      - data
      - event

SampleRequestPayload:
  description: "SampleRequestPayload"
  discriminator: event
  oneOf:
    - $ref: '#/components/schemas/GetUser'
    - $ref: '#/components/schemas/DeleteUser'

生成的 Rust 代码

#[derive(Debug, Clone, Eq, PartialEq, serde :: Deserialize, serde :: Serialize)]
pub struct RequestBase {
    #[serde(rename = "id")]
    id: String,
    #[serde(rename = "kind")]
    kind: Option<monostate::MustBe!("request")>,
    #[serde(rename = "tupleProp")]
    tuple_prop: Option<(String, serde_json::Value)>,
    #[serde(flatten)]
    additional_properties: std::collections::HashMap<String, Vec<serde_json::Value>>,
}

#[derive(Debug, Clone, Eq, PartialEq, serde :: Deserialize, serde :: Serialize)]
pub struct DeleteUserData {
    #[serde(rename = "userId")]
    user_id: String,
}

#[derive(Debug, Clone, Eq, PartialEq, serde :: Deserialize, serde :: Serialize)]
pub struct DeleteUserInner {
    #[serde(rename = "data")]
    data: DeleteUserData,
}

#[derive(Debug, Clone, Eq, PartialEq, serde :: Deserialize, serde :: Serialize)]
pub struct DeleteUser {
    #[serde(flatten)]
    request_base: RequestBase,
    #[serde(flatten)]
    delete_user_inner: DeleteUserInner,
}

#[derive(Debug, Clone, Eq, PartialEq, serde :: Deserialize, serde :: Serialize)]
pub struct GetUserData {
    #[serde(rename = "userId")]
    user_id: String,
    #[serde(rename = "name")]
    name: Option<String>,
}

#[derive(Debug, Clone, Eq, PartialEq, serde :: Deserialize, serde :: Serialize)]
pub struct GetUserInner {
    #[serde(rename = "data")]
    data: GetUserData,
}

#[derive(Debug, Clone, Eq, PartialEq, serde :: Deserialize, serde :: Serialize)]
pub struct GetUser {
    #[serde(flatten)]
    request_base: RequestBase,
    #[serde(flatten)]
    get_user_inner: GetUserInner,
}

#[derive(Debug, Clone, Eq, PartialEq, serde :: Deserialize, serde :: Serialize)]
#[serde(tag = "event")]
pub enum SampleRequestPayload {
    GetUser(GetUser),
    DeleteUser(DeleteUser),
}

依赖项

~5MB
~98K SLoC