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

Add Contract for generating separate serialize/deserialize schemas #335

Merged
merged 15 commits into from
Sep 4, 2024
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [1.0.0-alpha.15] - **in-dev**

### Added

- `SchemaSettings` now has a `contract` field which determines whether the generated schemas describe how types are serialized or *de*serialized. By default, this is set to `Deserialize`, as this more closely matches the behaviour of previous versions - you can change this to `Serialize` to instead generate schemas describing the type's serialization behaviour (https://github.com/GREsau/schemars/issues/48 / https://github.com/GREsau/schemars/pull/335)

### Changed

- Schemas generated for enums with no variants will now generate `false` (or equivalently `{"not":{}}`), instead of `{"enum":[]}`. This is so generated schemas no longer violate the JSON Schema spec's recommendation that a schema's `enum` array "SHOULD have at least one element".

## [1.0.0-alpha.14] - 2024-08-29

### Added
Expand Down
15 changes: 7 additions & 8 deletions docs/3-generating.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,14 @@ let my_schema = generator.into_root_schema_for::<MyStruct>();

See the API documentation for more info on how to use those types for custom schema generation.

### Serialize vs. Deserialize contract

Of particular note is the `contract` setting, which controls whether the generated schemas should describe how types are serialized or how they're *de*serialized. By default, this is set to `Deserialize`. If you instead want your schema to describe the serialization behaviour, modify the `contract` field of `SchemaSettings` or use the `for_serialize()` helper method:

{% include example.md name="serialize_contract" %}

## Schema from Example Value

If you want a schema for a type that can't/doesn't implement `JsonSchema`, but does implement `serde::Serialize`, then you can generate a JSON schema from a value of that type using the [`schema_for_value!` macro](https://docs.rs/schemars/1.0.0--latest/schemars/macro.schema_for_value.html). However, this schema will generally be less precise than if the type implemented `JsonSchema` - particularly when it involves enums, since schemars will not make any assumptions about the structure of an enum based on a single variant.

```rust
let value = MyStruct { foo = 123 };
let my_schema = schema_for_value!(value);
```

<!-- TODO:
create and link to example
-->
{% include example.md name="from_value" %}
29 changes: 29 additions & 0 deletions docs/_includes/examples/serialize_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use schemars::{generate::SchemaSettings, JsonSchema};
use serde::{Deserialize, Serialize};

#[derive(JsonSchema, Deserialize, Serialize)]
// The schema effectively ignores this `rename_all`, since it doesn't apply to serialization
#[serde(rename_all(deserialize = "PascalCase"))]
pub struct MyStruct {
pub my_int: i32,
#[serde(skip_deserializing)]
pub my_read_only_bool: bool,
// This property is excluded from the schema
#[serde(skip_serializing)]
pub my_write_only_bool: bool,
// This property is excluded from the "required" properties of the schema, because it may be
// be skipped during serialization
#[serde(skip_serializing_if = "str::is_empty")]
pub maybe_string: String,
pub definitely_string: String,
}

fn main() {
// By default, generated schemas describe how types are deserialized.
// So we modify the settings here to instead generate schemas describing how it's serialized:
let settings = SchemaSettings::default().for_serialize();

let generator = settings.into_generator();
let schema = generator.into_root_schema_for::<MyStruct>();
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
}
27 changes: 27 additions & 0 deletions docs/_includes/examples/serialize_contract.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "MyStruct",
"type": "object",
"properties": {
"definitely_string": {
"type": "string"
},
"maybe_string": {
"type": "string"
},
"my_int": {
"type": "integer",
"format": "int32"
},
"my_read_only_bool": {
"type": "boolean",
"default": false,
"readOnly": true
}
},
"required": [
"my_int",
"my_read_only_bool",
"definitely_string"
]
}
29 changes: 29 additions & 0 deletions schemars/examples/serialize_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use schemars::{generate::SchemaSettings, JsonSchema};
use serde::{Deserialize, Serialize};

#[derive(JsonSchema, Deserialize, Serialize)]
// The schema effectively ignores this `rename_all`, since it doesn't apply to serialization
#[serde(rename_all(deserialize = "PascalCase"))]
pub struct MyStruct {
pub my_int: i32,
#[serde(skip_deserializing)]
pub my_read_only_bool: bool,
// This property is excluded from the schema
#[serde(skip_serializing)]
pub my_write_only_bool: bool,
// This property is excluded from the "required" properties of the schema, because it may be
// be skipped during serialization
#[serde(skip_serializing_if = "str::is_empty")]
pub maybe_string: String,
pub definitely_string: String,
}

fn main() {
// By default, generated schemas describe how types are deserialized.
// So we modify the settings here to instead generate schemas describing how it's serialized:
let settings = SchemaSettings::default().for_serialize();

let generator = settings.into_generator();
let schema = generator.into_root_schema_for::<MyStruct>();
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
}
27 changes: 27 additions & 0 deletions schemars/examples/serialize_contract.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "MyStruct",
"type": "object",
"properties": {
"definitely_string": {
"type": "string"
},
"maybe_string": {
"type": "string"
},
"my_int": {
"type": "integer",
"format": "int32"
},
"my_read_only_bool": {
"type": "boolean",
"default": false,
"readOnly": true
}
},
"required": [
"my_int",
"my_read_only_bool",
"definitely_string"
]
}
46 changes: 17 additions & 29 deletions schemars/src/_private/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,42 +134,30 @@ pub fn apply_internal_enum_variant_tag(
}
}

pub fn insert_object_property<T: ?Sized + JsonSchema>(
pub fn insert_object_property(
schema: &mut Schema,
key: &str,
has_default: bool,
required: bool,
is_optional: bool,
sub_schema: Schema,
) {
fn insert_object_property_impl(
schema: &mut Schema,
key: &str,
has_default: bool,
required: bool,
sub_schema: Schema,
) {
let obj = schema.ensure_object();
if let Some(properties) = obj
.entry("properties")
.or_insert(Value::Object(Map::new()))
.as_object_mut()
{
properties.insert(key.to_owned(), sub_schema.into());
}
let obj = schema.ensure_object();
if let Some(properties) = obj
.entry("properties")
.or_insert(Value::Object(Map::new()))
.as_object_mut()
{
properties.insert(key.to_owned(), sub_schema.into());
}

if !has_default && (required) {
if let Some(req) = obj
.entry("required")
.or_insert(Value::Array(Vec::new()))
.as_array_mut()
{
req.push(key.into());
}
if !is_optional {
if let Some(req) = obj
.entry("required")
.or_insert(Value::Array(Vec::new()))
.as_array_mut()
{
req.push(key.into());
}
}

let required = required || !T::_schemars_private_is_option();
insert_object_property_impl(schema, key, has_default, required, sub_schema);
}

pub fn insert_metadata_property(schema: &mut Schema, key: &str, value: impl Into<Value>) {
Expand Down
Loading
Loading