schema2code

Generate Code in any language from a JSON Schema compliant document

5 releases

0.1.4 Sep 25, 2023
0.1.3 Sep 25, 2023
0.1.2 Sep 25, 2023
0.1.1 Sep 3, 2023
0.1.0 Sep 3, 2023

#528 in Development tools

MIT/Apache

41KB
799 lines

Schema Parser & Code Generator

A language agnostic model generator for a subset of JSON schemas. (Rust only for now, next planned: python + pydantic)

How to Use

Check out the codegen-test crate to see how it can be used to generate your code at compile time.

Schema constraints

While writing the parser and deserializer I put some constraints of possible schemas, this only allows a subset of possible asyncapi schema definitions.

  • Every top-level item in the components -> schemas part of the document needs to be an actual schema, this would not be allowed:
components:
  schemas:
    MyEntity:
      $ref: '#/components/schemas/AnotherEntity'
  • Every top-level schema can only be one of the following: [AllOf, OneOf, AnyOf, type: object], top-level array types don't currently work, in your asyncapi schema I'd recommend creating an anonymous schema in the messages part of the specification and creating the specific item type for the array items in the components/schemas section such that your code will have the type for the items and you can easily deserialize payloads by wrapping it in language specific collections.
  • Currently enums only work with String values, even if they're supported at deserialization/parsing at generation time numerical enums will throw errors as I haven't created a specific type to distinguish them from Literal enums.
  • Every time a const value is specified there must be a type with it.
  • Currently only integers are supported and any format directive is simply ignored
  • Due to how the current implementation of AllOf works duplicate properties will cause errors in Rust, the current codegenerator just takes the combined schemas, creates an AnonymousEntity for each (or a named one if title is set) and then combines them with #[serde(flatten)] in a struct, this will cause the deserialization to fail if the combined schemas define overlapping properties. (Fixing this is on my roadmap but not a priority, in OOP languages my codegenerator will simply extend all AllOf schema classes and duplicate properties will be handled by the inheritance of the programming language)

Sidenote for Rust users

  • For OneOf schemas with a specific discriminator set it currently only works if the discriminator matches the name of the entity (For anonymous entity set the title property for a deterministic name), otherwise if you use special values for the discriminator inside of const fields you need to omit the discriminator for now and just use the #[serde(untagged)] enum that is generated, const fields will be respected through the use of the monostate crate.
  • AllOf schemas currently don't merge properties, out of lazyness they create struct for inner schemas and then put them in a single struct through #[serde(flatten)]. (Out of simplicity I may use a solution like this in other languages, having a named empty class inherit from anonymous/named structs for its fields)

Planned

  • A CLI tool for code generation
  • Python pydantic model generator
  • A protobuf generator

Issues

The deserializer defines untagged enums with monostate::MustBe for the deserialization of a schema, this leads to quite unhelpful error messages when you schema does not match, most of the errors are Did not match any variant in SchemaDef

Sample

Asyncapi schema definitions:

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'

Generated rust code:

#[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),
}

Dependencies

~3.5–5MB
~101K SLoC