Skip to content

Commit

Permalink
Add Contract for generating separate serialize/deserialize schemas (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
GREsau authored Sep 4, 2024
1 parent 497333e commit 05325d2
Show file tree
Hide file tree
Showing 36 changed files with 1,222 additions and 223 deletions.
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

0 comments on commit 05325d2

Please sign in to comment.