schemars icon indicating copy to clipboard operation
schemars copied to clipboard

feat: unique types in root schema

Open miraclx opened this issue 3 years ago • 5 comments
trafficstars

Fixes #177.

Instead of using a 1-1 representation of type name to type schema, this patch uses a better discriminant. By casting a generic function pointer to a usize, we get a numeric representation of the pointer, which is dependent on the type T.

Meaning we can use that discriminant to better disambiguate type signatures.

The discriminant then informs the index in the definitions map.

definitions then becomes a map of index to Schema. With the type name moved inside the schema. Effectively a Vec<Schema> but adhering to the spec of it being a map.

miraclx avatar Oct 07 '22 19:10 miraclx

In https://github.com/GREsau/schemars/issues/177#issuecomment-1270932655, I suggested using std::any::TypeId and I tried that, but it requires T: 'static.

The current implementation returns a type discriminant that's inconsistent across runs, but hey, we don't need it to be. It just needs to inform the index in the definitions map.

miraclx avatar Oct 07 '22 19:10 miraclx

What's left is to decide how we're to proceed with this.

Should this be the default behavior? Otherwise, we're currently exporting a default that silently introduces inconsistencies.

CI fails currently because of this conflict, with tests that expect type keys to be Strings, yet is run with --all-features which activates this patch, and makes type keys usizes.

miraclx avatar Oct 08 '22 01:10 miraclx

Very nice! How do you feel about using ID+Name for better human consumption, i.e. something like #/definitions/1-MyType?

webmaster128 avatar Nov 23 '22 08:11 webmaster128

Actually, I thought about replacing this patch entirely with module scoped schema entries.

So, not a one-dimensional namespace, like it currently is. And not indexes like this patch does.

But instead, something like this (relative to the root crate):

mod mod_1 {
    #[derive(JsonSchema)]
    struct MyType;
}

#[derive(JsonSchema)]
struct MyType;

where mod_1::MyType gets the ref: #/definitions/:mod:/definitions/mod_1/definitions/MyType and MyType gets the ref: #/definitions/MyType

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "MyStruct",
  "type": "object",
  "required": [
    "my_bool",
    "my_int"
  ],
  "properties": {
    "my_bool": {
      "type": "boolean"
    },
    "my_int": {
      "type": "integer",
      "format": "int32"
    },
    "my_nullable_enum0": {
      "anyOf": [
        {
          "$ref": "#/definitions/MyType"
        },
        {
          "type": "null"
        }
      ]
    },
    "my_nullable_enum1": {
      "anyOf": [
        {
          "$ref": "#/definitions/:mod:/definitions/mod_1/definitions/MyType"
        },
        {
          "type": "null"
        }
      ]
    },
    "my_nullable_enum2": {
      "anyOf": [
        {
          "$ref": "#/definitions/:mod:/definitions/mod_2/definitions/MyType"
        },
        {
          "type": "null"
        }
      ]
    }
  },
  "definitions": {
    ":mod:": {
      "definitions": {
        "mod_1": {
          "definitions": {
            "MyType": {
              "type": "object",
              "required": [
                "field"
              ],
              "properties": {
                "field": {
                  "type": "string"
                }
              }
            }
          }
        },
        "mod_2": {
          "definitions": {
            "MyType": {
              "oneOf": [
                {
                  "type": "object",
                  "required": [
                    "StringNewType"
                  ],
                  "properties": {
                    "StringNewType": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": false
                },
                {
                  "type": "object",
                  "required": [
                    "StructVariant"
                  ],
                  "properties": {
                    "StructVariant": {
                      "type": "object",
                      "required": [
                        "floats"
                      ],
                      "properties": {
                        "floats": {
                          "type": "array",
                          "items": {
                            "type": "number",
                            "format": "float"
                          }
                        }
                      }
                    }
                  },
                  "additionalProperties": false
                }
              ]
            }
          }
        }
      }
    },
    "MyType": {
      "type": "array",
      "items": [
        {
          "type": "string"
        },
        {
          "type": "array",
          "items": {
            "type": "integer",
            "format": "uint8",
            "minimum": 0.0
          }
        }
      ],
      "maxItems": 2,
      "minItems": 2
    }
  }
}

But first, that requires patching the JsonSchema trait, and for the derive macro, it's quite tricky to auto-infer module paths. And it forces JsonSchema implementors to define scopes locally.

miraclx avatar Nov 23 '22 10:11 miraclx

Hi! The "concretized generic function pointer as type ID" thing is brilliant. Stealing that for jtd-derive.

Another approach I suspect you could try is to use those type IDs to reliably detect name collisions between definitions, throw an error in case of trouble, and provide some way to disambiguate same-named types.

uint avatar Dec 31 '22 17:12 uint