-
-
Notifications
You must be signed in to change notification settings - Fork 250
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
Support generating multiple schemas (for serialize vs deserialize) from the same struct #48
Comments
Interesting idea, this could indeed be useful. I would certainly consider altering the behaviour of This would also fix #25, since aliases are only significant when deserializing |
Has there been any discussion on how this could be implemented? As far as I can tell, generating examples is done via Serde serialize. This would mean, it's necessary to create at least one copy (as in generating a new struct at compiletime in a macro, not copying in the sense of |
Some thoughts I've had on how to deal with this:Related issues:
The current state of playExample 1#[derive(JsonSchema, Serialize, Deserialize)]
pub struct MyStruct {
#[serde(alias = "my_i32")]
pub my_int: i32,
pub my_bool: bool,
} The current schema produced by schemars is: {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "MyStruct",
"type": "object",
"properties": {
"my_bool": {
"type": "boolean"
},
"my_int": {
"type": "integer",
"format": "int32"
}
},
"required": ["my_int", "my_bool"]
} This schema does not accurately describe the deserialization contract of the type when using serde_json, because it would not successfully validate a value using the {
"my_i32": 123,
"my_bool": false
} While the schema does "correctly" describe the serialization contract of the type (in that all possible serialized values would successfully validate against the schema), the schema is not "specific" because it also allows unknown properties. The schema could be made more specific by including Example 2#[derive(JsonSchema, Serialize, Deserialize)]
pub struct MyStruct2 {
#[serde(skip_serializing)]
pub write_only: i32,
#[serde(skip_deserializing)]
pub read_only: bool,
} {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "MyStruct2",
"type": "object",
"properties": {
"read_only": {
"type": "boolean",
"default": false,
"readOnly": true
},
"write_only": {
"type": "integer",
"format": "int32",
"writeOnly": true
}
},
"required": ["write_only"]
} This uses the This schema arguably does not quite accurately describe the deserialization contract because it restricts the And this schema does not accurately describe the serialization contract because it includes In summary: Schemars currently isn't sure whether it should be describing the serialization or deserialization contract for a type, so it ends up doing both of them quite badly. Potential Solution - Extending
|
So, let's say for sake of argument that we go with extending the schemars API like so: pub struct SchemaSettings {
/* snip: existing SchemaSettings fields... */
pub contract: SchemaContract,
pub deny_unknown_fields: bool,
}
pub enum SchemaContract {
Serialize,
Deserialize,
} Then what would the schema for the examples in the previous comment look like? Example 1#[derive(JsonSchema, Serialize, Deserialize)]
pub struct MyStruct {
#[serde(alias = "my_i32")]
pub my_int: i32,
pub my_bool: bool,
} When using {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "MyStruct",
"type": "object",
"properties": {
"my_int": {
"type": "integer",
"format": "int32"
},
"my_bool": {
"type": "boolean"
}
},
"required": [
"my_int",
"my_bool"
],
"additionalProperties": false /* could use "unevaluatedProperties" instead of "additionalProperties" */
} When using {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "MyStruct",
"type": "object",
"properties": {
"my_bool": {
"type": "boolean"
}
},
"oneOf": [
{
"properties": {
"my_int": {
"type": "integer",
"format": "int32"
}
},
"required": [
"my_int"
]
},
{
"properties": {
"my_i32": {
"type": "integer",
"format": "int32"
}
},
"required": [
"my_i32"
]
}
],
"required": [
"my_bool"
]
} Or, it could include the alias schemas in the top-level {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "MyStruct",
"type": "object",
"properties": {
"my_int": {
"type": "integer",
"format": "int32"
},
"my_i32": {
"type": "integer",
"format": "int32"
},
"my_bool": {
"type": "boolean"
}
},
"oneOf": [
{
"required": [
"my_int"
]
},
{
"required": [
"my_i32"
]
}
],
"required": [
"my_bool"
]
} When using {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "MyStruct",
"type": "object",
"properties": {
"my_bool": {
"type": "boolean"
}
},
"oneOf": [
{
"properties": {
"my_int": {
"type": "integer",
"format": "int32"
}
},
"required": [
"my_int"
]
},
{
"properties": {
"my_i32": {
"type": "integer",
"format": "int32"
}
},
"required": [
"my_i32"
]
}
],
"required": [
"my_bool"
],
"unevaluatedProperties": false
} By putting them in the top-level {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "MyStruct",
"type": "object",
"properties": {
"my_int": {
"type": "integer",
"format": "int32"
},
"my_i32": {
"type": "integer",
"format": "int32"
},
"my_bool": {
"type": "boolean"
}
},
"oneOf": [
{
"required": [
"my_int"
]
},
{
"required": [
"my_i32"
]
}
],
"required": [
"my_bool"
],
"additionalProperties": false /* could use "unevaluatedProperties" instead of "additionalProperties" */
} Example 2#[derive(JsonSchema, Serialize, Deserialize)]
pub struct MyStruct2 {
#[serde(skip_serializing)]
pub write_only: i32,
#[serde(skip_deserializing)]
pub read_only: bool,
} When using {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "MyStruct",
"type": "object",
"properties": {
"read_only": {
"type": "boolean",
"default": false,
"readOnly": true
}
},
"required": [
"read_only"
],
"additionalProperties": false /* could use "unevaluatedProperties" instead of "additionalProperties" */
} Conversely, {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "MyStruct",
"type": "object",
"properties": {
"write_only": {
"type": "integer",
"format": "int32",
"writeOnly": true
}
},
"required": [
"write_only"
]
} And with {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "MyStruct",
"type": "object",
"properties": {
"write_only": {
"type": "integer",
"format": "int32",
"writeOnly": true
}
},
"required": [
"write_only"
],
"additionalProperties": false /* could use "unevaluatedProperties" instead of "additionalProperties" */
} |
Although something else to keep in mind is how to support use-cases that want to generate schemas for both the "serialize" and "deserialize" contract, for example when generating an openapi document. Currently, there's no way to modify the But we would still have a problem if you try to generate both a "serialize" and "deserialize" schema for the same type - the first one you generate would add the schema to the definitions ( |
Changes requires in schemars_derive:
|
I like how you started with the conceptual definitons. Probably makes sense to have clarity on those “guiding principles” before moving to specific API decisions like Re. “the Serialize schema generated for a type T should successfully validate a JSON value if and only if there exists some possible value of T that serializes to that JSON value”, there’s also the component of change over time :) I wonder if a instead of |
That's interesting, I hadn't considered that scenario. Although obviously, we have to limit what we say would be "acceptable" as a change over time. Adding new properties is probably reasonable, hence we can allow unknown properties generally (as we do today for structs without So that specific scenario (allowing future/unknown properties) would be possible if we either had the
That's essentially the "Separate flags on SchemaSettings" alternative design I mentioned above. I'm concerned about how easy it would be to scale and reason about how they would all interact with each other given the high number of serde attributes that we need to consider. We would also need to consider how to deal with types that implement |
This is now implemented in 1.0.0-alpha.15, and documented at https://graham.cool/schemars/generating/#serialize-vs-deserialize-contract. TLDR: by default it generates "deserialize" schemas, if you instead want a "serialize" schema then do this: let schema = SchemaSettings::default()
.for_serialize()
.into_generator()
.into_root_schema_for::<MyStruct>(); However, |
I have a lot of types whose deserialization behavior allows for more types than the serialization does. For example:
Therefore I would like to generate two schemas per type: A schema describing the input that is allowed, and a schema that describes the output that is guaranteed.
Is this kind of feature something you'd be interested in merging? I am on the fence as to whether this is sensible. In some languages I would've created separate input and output types, but as serde allowed me to do everything with one type this is what I ended up with. But I think that adding this feature to schemars is easier than refactoring my existing codebase.
The text was updated successfully, but these errors were encountered: