schemars
schemars copied to clipboard
feat: unique types in root schema
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.
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.
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.
Very nice! How do you feel about using ID+Name for better human consumption, i.e. something like #/definitions/1-MyType?
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.
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.