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

Generate schema from any serializable value #75

Merged
merged 8 commits into from
Mar 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,68 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap());

`#[serde(...)]` attributes can be overriden using `#[schemars(...)]` attributes, which behave identically (e.g. `#[schemars(rename_all = "camelCase")]`). You may find this useful if you want to change the generated schema without affecting Serde's behaviour, or if you're just not using Serde.

### Schema from Example Values

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. 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
use schemars::schema_for_value;
use serde::Serialize;

#[derive(Serialize)]
pub struct MyStruct {
pub my_int: i32,
pub my_bool: bool,
pub my_nullable_enum: Option<MyEnum>,
}

#[derive(Serialize)]
pub enum MyEnum {
StringNewType(String),
StructVariant { floats: Vec<f32> },
}

fn main() {
let schema = schema_for_value!(MyStruct {
my_int: 123,
my_bool: true,
my_nullable_enum: Some(MyEnum::StringNewType("foo".to_string()))
});
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
}
```

<details>
<summary>Click to see the output JSON schema...</summary>

```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MyStruct",
"examples": [
{
"my_bool": true,
"my_int": 123,
"my_nullable_enum": {
"StringNewType": "foo"
}
}
],
"type": "object",
"properties": {
"my_bool": {
"type": "boolean"
},
"my_int": {
"type": "integer",
"format": "int32"
},
"my_nullable_enum": true
}
}
```
</details>

## Feature Flags
- `derive` (enabled by default) - provides `#[derive(JsonSchema)]` macro
- `impl_json_schema` - implements `JsonSchema` for Schemars types themselves
Expand Down
4 changes: 3 additions & 1 deletion docs/3-generating.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ If you want more control over how the schema is generated, you can use the [`gen

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

<!-- TODO:
<!-- TODO:
create and link to example

Generating schema from example value
-->
24 changes: 24 additions & 0 deletions docs/_includes/examples/from_value.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use schemars::schema_for_value;
use serde::Serialize;

#[derive(Serialize)]
pub struct MyStruct {
pub my_int: i32,
pub my_bool: bool,
pub my_nullable_enum: Option<MyEnum>,
}

#[derive(Serialize)]
pub enum MyEnum {
StringNewType(String),
StructVariant { floats: Vec<f32> },
}

fn main() {
let schema = schema_for_value!(MyStruct {
my_int: 123,
my_bool: true,
my_nullable_enum: Some(MyEnum::StringNewType("foo".to_string()))
});
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
}
24 changes: 24 additions & 0 deletions docs/_includes/examples/from_value.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MyStruct",
"examples": [
{
"my_bool": true,
"my_int": 123,
"my_nullable_enum": {
"StringNewType": "foo"
}
}
],
"type": "object",
"properties": {
"my_bool": {
"type": "boolean"
},
"my_int": {
"type": "integer",
"format": "int32"
},
"my_nullable_enum": true
}
}
24 changes: 24 additions & 0 deletions schemars/examples/from_value.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use schemars::schema_for_value;
use serde::Serialize;

#[derive(Serialize)]
pub struct MyStruct {
pub my_int: i32,
pub my_bool: bool,
pub my_nullable_enum: Option<MyEnum>,
}

#[derive(Serialize)]
pub enum MyEnum {
StringNewType(String),
StructVariant { floats: Vec<f32> },
}

fn main() {
let schema = schema_for_value!(MyStruct {
my_int: 123,
my_bool: true,
my_nullable_enum: Some(MyEnum::StringNewType("foo".to_string()))
});
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
}
24 changes: 24 additions & 0 deletions schemars/examples/from_value.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MyStruct",
"examples": [
{
"my_bool": true,
"my_int": 123,
"my_nullable_enum": {
"StringNewType": "foo"
}
}
],
"type": "object",
"properties": {
"my_bool": {
"type": "boolean"
},
"my_int": {
"type": "integer",
"format": "int32"
},
"my_nullable_enum": true
}
}
3 changes: 3 additions & 0 deletions schemars/src/flatten.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ use crate::schema::*;
use crate::{Map, Set};

impl Schema {
/// This function is only public for use by schemars_derive.
///
/// It should not be considered part of the public API.
#[doc(hidden)]
pub fn flatten(self, other: Self) -> Schema {
if is_null_type(&self) {
Expand Down
67 changes: 66 additions & 1 deletion schemars/src/gen.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*!
JSON Schema generator and settings.

This module is useful if you want more control over how the schema generated then the [`schema_for!`] macro gives you.
This module is useful if you want more control over how the schema generated than the [`schema_for!`] macro gives you.
There are two main types in this module:two main types in this module:
* [`SchemaSettings`], which defines what JSON Schema features should be used when generating schemas (for example, how `Option`s should be represented).
* [`SchemaGenerator`], which manages the generation of a schema document.
Expand All @@ -11,6 +11,7 @@ use crate::flatten::Merge;
use crate::schema::*;
use crate::{visit::*, JsonSchema, Map};
use dyn_clone::DynClone;
use serde::Serialize;
use std::{any::Any, collections::HashSet, fmt::Debug};

/// Settings to customize how Schemas are generated.
Expand Down Expand Up @@ -314,6 +315,70 @@ impl SchemaGenerator {
root
}

/// Generates a root JSON Schema for the given example value.
///
/// If the value implements [`JsonSchema`](crate::JsonSchema), then prefer using the [`root_schema_for()`](Self::root_schema_for())
/// function which will generally produce a more precise schema, particularly when the value contains any enums.
pub fn root_schema_for_value<T: ?Sized + Serialize>(
&mut self,
value: &T,
) -> Result<RootSchema, serde_json::Error> {
let mut schema = value
.serialize(crate::ser::Serializer {
gen: self,
include_title: true,
})?
.into_object();

if let Ok(example) = serde_json::to_value(value) {
schema.metadata().examples.push(example);
}

let mut root = RootSchema {
meta_schema: self.settings.meta_schema.clone(),
definitions: self.definitions.clone(),
schema,
};

for visitor in &mut self.settings.visitors {
visitor.visit_root_schema(&mut root)
}

Ok(root)
}

/// Consumes `self` and generates a root JSON Schema for the given example value.
///
/// If the value implements [`JsonSchema`](crate::JsonSchema), then prefer using the [`into_root_schema_for()!`](Self::into_root_schema_for())
/// function which will generally produce a more precise schema, particularly when the value contains any enums.
pub fn into_root_schema_for_value<T: ?Sized + Serialize>(
mut self,
value: &T,
) -> Result<RootSchema, serde_json::Error> {
let mut schema = value
.serialize(crate::ser::Serializer {
gen: &mut self,
include_title: true,
})?
.into_object();

if let Ok(example) = serde_json::to_value(value) {
schema.metadata().examples.push(example);
}

let mut root = RootSchema {
meta_schema: self.settings.meta_schema,
definitions: self.definitions,
schema,
};

for visitor in &mut self.settings.visitors {
visitor.visit_root_schema(&mut root)
}

Ok(root)
}

/// Attemps to find the schema that the given `schema` is referencing.
///
/// If the given `schema` has a [`$ref`](../schema/struct.SchemaObject.html#structfield.reference) property which refers
Expand Down
1 change: 1 addition & 0 deletions schemars/src/json_schema_impls/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ impl<T: JsonSchema> JsonSchema for Option<T> {
schema
}
schema => SchemaObject {
// TODO technically the schema already accepts null, so this may be unnecessary
subschemas: Some(Box::new(SubschemaValidation {
any_of: Some(vec![schema, <()>::json_schema(gen)]),
..Default::default()
Expand Down
67 changes: 65 additions & 2 deletions schemars/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,71 @@ println!("{}", serde_json::to_string_pretty(&schema).unwrap());

`#[serde(...)]` attributes can be overriden using `#[schemars(...)]` attributes, which behave identically (e.g. `#[schemars(rename_all = "camelCase")]`). You may find this useful if you want to change the generated schema without affecting Serde's behaviour, or if you're just not using Serde.

### Schema from Example Values

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. 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
use schemars::schema_for_value;
use serde::Serialize;

#[derive(Serialize)]
pub struct MyStruct {
pub my_int: i32,
pub my_bool: bool,
pub my_nullable_enum: Option<MyEnum>,
}

#[derive(Serialize)]
pub enum MyEnum {
StringNewType(String),
StructVariant { floats: Vec<f32> },
}

fn main() {
let schema = schema_for_value!(MyStruct {
my_int: 123,
my_bool: true,
my_nullable_enum: Some(MyEnum::StringNewType("foo".to_string()))
});
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
}
```

<details>
<summary>Click to see the output JSON schema...</summary>

```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MyStruct",
"examples": [
{
"my_bool": true,
"my_int": 123,
"my_nullable_enum": {
"StringNewType": "foo"
}
}
],
"type": "object",
"properties": {
"my_bool": {
"type": "boolean"
},
"my_int": {
"type": "integer",
"format": "int32"
},
"my_nullable_enum": true
}
}
```
</details>

## Feature Flags
- `derive` (enabled by default) - provides `#[derive(JsonSchema)]` macro.
- `impl_json_schema` - implements `JsonSchema` for Schemars types themselves.
- `derive` (enabled by default) - provides `#[derive(JsonSchema)]` macro
- `impl_json_schema` - implements `JsonSchema` for Schemars types themselves
- `preserve_order` - keep the order of struct fields in `Schema` and `SchemaObject`

## Optional Dependencies
Expand Down Expand Up @@ -236,6 +298,7 @@ pub type MapEntry<'a, K, V> = indexmap::map::Entry<'a, K, V>;

mod flatten;
mod json_schema_impls;
mod ser;
#[macro_use]
mod macros;

Expand Down
Loading