Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Internally tagged enums won't generate semantically correct schema #358

Closed
WhySoBad opened this issue Dec 6, 2024 · 3 comments
Closed

Comments

@WhySoBad
Copy link

WhySoBad commented Dec 6, 2024

This issue kind of a regression in version 1.0.0-alpha.17 as in 1.0.0-alpha.16 everything still works as expected. With the addition of #355 internally tagged enums now use references to other definitions and no longer inline all object definitions. In the way how these references are applied there seems to be an issue.

When creating an enum of structs which are internally tagged (e.g. "type" as tag) the generated schema only suggests the type property and not the properties of the structs themselves.

I used the following minimal code example to reproduce the issue:

main.rs
use std::fs;

use schemars::{schema_for, JsonSchema};
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, JsonSchema)]
struct First {
    name: String,
    age: u32
}

#[derive(Deserialize, Serialize, JsonSchema)]
struct Second {
    place: String,
    street: String
}

#[derive(Deserialize, Serialize, JsonSchema)]
struct Third {
    address: String,
    zip: u32
}

#[derive(Deserialize, Serialize, JsonSchema)]
#[schemars(tag = "type", rename_all = "lowercase")]
enum MyEnum {
    First(First),
    Second(Second),
    Third(Third)
}

#[derive(Deserialize, Serialize, JsonSchema)]
struct MySchema {
    test: String,
    other: MyEnum
}

fn main() {
    let schema = schema_for!(MySchema);
    let json = serde_json::to_string_pretty(&schema).expect("should create string");
    fs::write("./schema.json", json).expect("should write schema.json");
    println!("Wrote to schema.json");
}
Cargo.toml
[package]
name = "schemars-test"
version = "0.1.0"
edition = "2021"

[dependencies]
schemars = "=1.0.0-alpha.17"
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.133"
Output schema.json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "MySchema",
  "type": "object",
  "properties": {
    "other": {
      "$ref": "#/$defs/MyEnum"
    },
    "test": {
      "type": "string"
    }
  },
  "required": [
    "test",
    "other"
  ],
  "$defs": {
    "First": {
      "type": "object",
      "properties": {
        "age": {
          "type": "integer",
          "format": "uint32",
          "minimum": 0
        },
        "name": {
          "type": "string"
        }
      },
      "required": [
        "name",
        "age"
      ]
    },
    "MyEnum": {
      "oneOf": [
        {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "const": "first"
            }
          },
          "$ref": "#/$defs/First",
          "required": [
            "type"
          ]
        },
        {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "const": "second"
            }
          },
          "$ref": "#/$defs/Second",
          "required": [
            "type"
          ]
        },
        {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "const": "third"
            }
          },
          "$ref": "#/$defs/Third",
          "required": [
            "type"
          ]
        }
      ]
    },
    "Second": {
      "type": "object",
      "properties": {
        "place": {
          "type": "string"
        },
        "street": {
          "type": "string"
        }
      },
      "required": [
        "place",
        "street"
      ]
    },
    "Third": {
      "type": "object",
      "properties": {
        "address": {
          "type": "string"
        },
        "zip": {
          "type": "integer",
          "format": "uint32",
          "minimum": 0
        }
      },
      "required": [
        "address",
        "zip"
      ]
    }
  }
}

As far as I understand JSON schemas I come to the conclusion that the error lies in these parts:

{
  "type": "object",
  "properties": {
    "type": {
      "type": "string",
      "const": "first"
    }
  },
  "$ref": "#/$defs/First",
  "required": [
    "type"
  ]
}

When I replace this with

{
  "type": "object",
  "properties": {
    "type": {
      "type": "string",
      "const": "first"
    }
  },
  "allOf": [
    { "$ref": "#/$defs/First" }
  ],
  "required": [
    "type"
  ]
}

The schema works as expected. Therefore, I would suggest wrapping those references in an allOf block.

Thanks a lot for this crate. It is very useful for defining JSON schemas for configuration files etc. for rust projects :)

@WhySoBad
Copy link
Author

WhySoBad commented Dec 6, 2024

After some further investigation I found the root of the problem:
When including a $ref the referenced schema gets "copied" to the current location and all keywords which are defined on the same level of the reference can be used to override properties of the referenced object.

Therefore, the generated schema is correct but it's not semantically what one would expect as the properties field overrides the properties of the referenced object which makes the referencing basically useless.

When using additionalProperties instead of properties this problem is resolved but it's a bad solution as one could potentially have an object in the #defs part of the schema which itself references another reference and defines additionalPropertes to circumvent the problem mentioned above which then could be overridden.

I see the following potential solutions:

  1. Usage of allOf field as one had to do prior to draft 08 (see initial comment)
  2. Inline all references (as one could do manually at the moment using the SchemaSettings struct)
  3. Use additionalProperties instead of properties in object definitions which use a $ref (this option is less of a solution but more a quick "fix" which makes the problem less likely)

I'm new to JSON schemas so there is a good chance I missed something but I have the feeling using the allOf field is the safest as by using $ref directly there always is the problem of accidentally overriding properties of the referenced schema as it's not possible to make any assumptions about the fields used in the referenced schema.

@WhySoBad WhySoBad changed the title Internally tagged enums won't generate correct schema Internally tagged enums won't generate semantically correct schema Dec 6, 2024
@GREsau
Copy link
Owner

GREsau commented Dec 7, 2024

When including a $ref the referenced schema gets "copied" to the current location and all keywords which are defined on the same level of the reference can be used to override properties of the referenced object.

I don't believe that's correct - my understanding is that the correct validation result is the combination of the "local" keywords and the $ref target keywords.

What are you using the process/consume the output schema? Perhaps the validation tool you're using doesn't support the latest version (draft 2020-12) of JSON schema? If that's the case, you can configure schemars to output schemas in an older format, e.g.

let settings = SchemaSettings::draft07();
let generator = settings.into_generator();
let schema = generator.into_root_schema_for::<MySchema>();

@WhySoBad
Copy link
Author

WhySoBad commented Dec 7, 2024

You're absolutely right. I wrongly assumed a maintained yaml language server would support the 2020-12 draft as of now but I was wrong.

I tested the schema using the vscode and zed language servers. After a bit of research I found out that I was using the yaml-language-server developed by RedHat in both editors. As it turns out this language server does not support drafts newer than draft 7 which explains why the schema didn't work in both editors.

Thanks for the quick reply and sorry for the troubles

@WhySoBad WhySoBad closed this as completed Dec 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants