Generate schema from any serializable value (#75)
Implement schema_for_value!(...) macro
This commit is contained in:
parent
0957204bc1
commit
f6482fd460
19 changed files with 1179 additions and 5 deletions
62
README.md
62
README.md
|
@ -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
|
||||
|
|
|
@ -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
docs/_includes/examples/from_value.rs
Normal file
24
docs/_includes/examples/from_value.rs
Normal file
|
@ -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
docs/_includes/examples/from_value.schema.json
Normal file
24
docs/_includes/examples/from_value.schema.json
Normal file
|
@ -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
schemars/examples/from_value.rs
Normal file
24
schemars/examples/from_value.rs
Normal file
|
@ -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
schemars/examples/from_value.schema.json
Normal file
24
schemars/examples/from_value.schema.json
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
|
|
@ -13,9 +13,66 @@
|
|||
///
|
||||
/// let my_schema = schema_for!(MyStruct);
|
||||
/// ```
|
||||
#[cfg(doc)]
|
||||
#[macro_export]
|
||||
macro_rules! schema_for {
|
||||
($type:ty) => {
|
||||
$crate::gen::SchemaGenerator::default().into_root_schema_for::<$type>()
|
||||
};
|
||||
}
|
||||
|
||||
/// Generates a [`RootSchema`](crate::schema::RootSchema) for the given type using default settings.
|
||||
///
|
||||
/// The type must implement [`JsonSchema`](crate::JsonSchema).
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use schemars::{schema_for, JsonSchema};
|
||||
///
|
||||
/// #[derive(JsonSchema)]
|
||||
/// struct MyStruct {
|
||||
/// foo: i32,
|
||||
/// }
|
||||
///
|
||||
/// let my_schema = schema_for!(MyStruct);
|
||||
/// ```
|
||||
#[cfg(not(doc))]
|
||||
#[macro_export]
|
||||
macro_rules! schema_for {
|
||||
($type:ty) => {
|
||||
$crate::gen::SchemaGenerator::default().into_root_schema_for::<$type>()
|
||||
};
|
||||
($_:expr) => {
|
||||
compile_error!("This argument to `schema_for!` is not a type - did you mean to use `schema_for_value!` instead?")
|
||||
};
|
||||
}
|
||||
|
||||
/// Generates a [`RootSchema`](crate::schema::RootSchema) for the given example value using default settings.
|
||||
///
|
||||
/// The value must implement [`Serialize`](serde::Serialize). If the value also implements [`JsonSchema`](crate::JsonSchema),
|
||||
/// then prefer using the [`schema_for!`](schema_for) macro which will generally produce a more precise schema,
|
||||
/// particularly when the value contains any enums.
|
||||
///
|
||||
/// If the `Serialize` implementation of the value decides to fail, this macro will panic.
|
||||
/// For a non-panicking alternative, create a [`SchemaGenerator`](crate::gen::SchemaGenerator) and use
|
||||
/// its [`into_root_schema_for_value`](crate::gen::SchemaGenerator::into_root_schema_for_value) method.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use schemars::schema_for_value;
|
||||
///
|
||||
/// #[derive(serde::Serialize)]
|
||||
/// struct MyStruct {
|
||||
/// foo: i32,
|
||||
/// }
|
||||
///
|
||||
/// let my_schema = schema_for_value!(MyStruct { foo: 123 });
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! schema_for_value {
|
||||
($value:expr) => {
|
||||
$crate::gen::SchemaGenerator::default()
|
||||
.into_root_schema_for_value(&$value)
|
||||
.unwrap()
|
||||
};
|
||||
}
|
||||
|
|
496
schemars/src/ser.rs
Normal file
496
schemars/src/ser.rs
Normal file
|
@ -0,0 +1,496 @@
|
|||
use crate::schema::*;
|
||||
use crate::JsonSchema;
|
||||
use crate::{gen::SchemaGenerator, Map};
|
||||
use serde_json::{Error, Value};
|
||||
use std::{convert::TryInto, fmt::Display};
|
||||
|
||||
pub(crate) struct Serializer<'a> {
|
||||
pub(crate) gen: &'a mut SchemaGenerator,
|
||||
pub(crate) include_title: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct SerializeSeq<'a> {
|
||||
gen: &'a mut SchemaGenerator,
|
||||
items: Option<Schema>,
|
||||
}
|
||||
|
||||
pub(crate) struct SerializeTuple<'a> {
|
||||
gen: &'a mut SchemaGenerator,
|
||||
items: Vec<Schema>,
|
||||
title: &'static str,
|
||||
}
|
||||
|
||||
pub(crate) struct SerializeMap<'a> {
|
||||
gen: &'a mut SchemaGenerator,
|
||||
properties: Map<String, Schema>,
|
||||
current_key: Option<String>,
|
||||
title: &'static str,
|
||||
}
|
||||
|
||||
macro_rules! forward_to_subschema_for {
|
||||
($fn:ident, $ty:ty) => {
|
||||
fn $fn(self, _value: $ty) -> Result<Self::Ok, Self::Error> {
|
||||
Ok(self.gen.subschema_for::<$ty>())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl<'a> serde::Serializer for Serializer<'a> {
|
||||
type Ok = Schema;
|
||||
type Error = Error;
|
||||
|
||||
type SerializeSeq = SerializeSeq<'a>;
|
||||
type SerializeTuple = SerializeTuple<'a>;
|
||||
type SerializeTupleStruct = SerializeTuple<'a>;
|
||||
type SerializeTupleVariant = Self;
|
||||
type SerializeMap = SerializeMap<'a>;
|
||||
type SerializeStruct = SerializeMap<'a>;
|
||||
type SerializeStructVariant = Self;
|
||||
|
||||
forward_to_subschema_for!(serialize_bool, bool);
|
||||
forward_to_subschema_for!(serialize_i8, i8);
|
||||
forward_to_subschema_for!(serialize_i16, i16);
|
||||
forward_to_subschema_for!(serialize_i32, i32);
|
||||
forward_to_subschema_for!(serialize_i64, i64);
|
||||
forward_to_subschema_for!(serialize_i128, i128);
|
||||
forward_to_subschema_for!(serialize_u8, u8);
|
||||
forward_to_subschema_for!(serialize_u16, u16);
|
||||
forward_to_subschema_for!(serialize_u32, u32);
|
||||
forward_to_subschema_for!(serialize_u64, u64);
|
||||
forward_to_subschema_for!(serialize_u128, u128);
|
||||
forward_to_subschema_for!(serialize_f32, f32);
|
||||
forward_to_subschema_for!(serialize_f64, f64);
|
||||
forward_to_subschema_for!(serialize_char, char);
|
||||
forward_to_subschema_for!(serialize_str, &str);
|
||||
forward_to_subschema_for!(serialize_bytes, &[u8]);
|
||||
|
||||
fn collect_str<T: ?Sized>(self, _value: &T) -> Result<Self::Ok, Self::Error>
|
||||
where
|
||||
T: Display,
|
||||
{
|
||||
Ok(self.gen.subschema_for::<String>())
|
||||
}
|
||||
|
||||
fn collect_map<K, V, I>(self, iter: I) -> Result<Self::Ok, Self::Error>
|
||||
where
|
||||
K: serde::Serialize,
|
||||
V: serde::Serialize,
|
||||
I: IntoIterator<Item = (K, V)>,
|
||||
{
|
||||
let value_schema = iter
|
||||
.into_iter()
|
||||
.try_fold(None, |acc, (_, v)| {
|
||||
if acc == Some(Schema::Bool(true)) {
|
||||
return Ok(acc);
|
||||
}
|
||||
|
||||
let schema = v.serialize(Serializer {
|
||||
gen: self.gen,
|
||||
include_title: false,
|
||||
})?;
|
||||
Ok(match &acc {
|
||||
None => Some(schema),
|
||||
Some(items) if items != &schema => Some(Schema::Bool(true)),
|
||||
_ => acc,
|
||||
})
|
||||
})?
|
||||
.unwrap_or(Schema::Bool(true));
|
||||
|
||||
Ok(SchemaObject {
|
||||
instance_type: Some(InstanceType::Object.into()),
|
||||
object: Some(Box::new(ObjectValidation {
|
||||
additional_properties: Some(Box::new(value_schema)),
|
||||
..ObjectValidation::default()
|
||||
})),
|
||||
..SchemaObject::default()
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
|
||||
Ok(self.gen.subschema_for::<Option<Value>>())
|
||||
}
|
||||
|
||||
fn serialize_some<T: ?Sized>(mut self, value: &T) -> Result<Self::Ok, Self::Error>
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
// FIXME nasty duplication of `impl JsonSchema for Option<T>`
|
||||
fn add_null_type(instance_type: &mut SingleOrVec<InstanceType>) {
|
||||
match instance_type {
|
||||
SingleOrVec::Single(ty) if **ty != InstanceType::Null => {
|
||||
*instance_type = vec![**ty, InstanceType::Null].into()
|
||||
}
|
||||
SingleOrVec::Vec(ty) if !ty.contains(&InstanceType::Null) => {
|
||||
ty.push(InstanceType::Null)
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
let mut schema = value.serialize(Serializer {
|
||||
gen: self.gen,
|
||||
include_title: false,
|
||||
})?;
|
||||
|
||||
if self.gen.settings().option_add_null_type {
|
||||
schema = match schema {
|
||||
Schema::Bool(true) => Schema::Bool(true),
|
||||
Schema::Bool(false) => <()>::json_schema(&mut self.gen),
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(ref mut instance_type),
|
||||
..
|
||||
}) => {
|
||||
add_null_type(instance_type);
|
||||
schema
|
||||
}
|
||||
schema => SchemaObject {
|
||||
subschemas: Some(Box::new(SubschemaValidation {
|
||||
any_of: Some(vec![schema, <()>::json_schema(&mut self.gen)]),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
||||
if self.gen.settings().option_nullable {
|
||||
let mut schema_obj = schema.into_object();
|
||||
schema_obj
|
||||
.extensions
|
||||
.insert("nullable".to_owned(), serde_json::json!(true));
|
||||
schema = Schema::Object(schema_obj);
|
||||
};
|
||||
|
||||
Ok(schema)
|
||||
}
|
||||
|
||||
fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
|
||||
Ok(self.gen.subschema_for::<()>())
|
||||
}
|
||||
|
||||
fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
|
||||
self.serialize_unit()
|
||||
}
|
||||
|
||||
fn serialize_unit_variant(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_variant_index: u32,
|
||||
_variant: &'static str,
|
||||
) -> Result<Self::Ok, Self::Error> {
|
||||
Ok(Schema::Bool(true))
|
||||
}
|
||||
|
||||
fn serialize_newtype_struct<T: ?Sized>(
|
||||
self,
|
||||
name: &'static str,
|
||||
value: &T,
|
||||
) -> Result<Self::Ok, Self::Error>
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
let include_title = self.include_title;
|
||||
let mut result = value.serialize(self);
|
||||
|
||||
if include_title {
|
||||
if let Ok(Schema::Object(ref mut object)) = result {
|
||||
object.metadata().title = Some(name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn serialize_newtype_variant<T: ?Sized>(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_variant_index: u32,
|
||||
_variant: &'static str,
|
||||
_value: &T,
|
||||
) -> Result<Self::Ok, Self::Error>
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
Ok(Schema::Bool(true))
|
||||
}
|
||||
|
||||
fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
|
||||
Ok(SerializeSeq {
|
||||
gen: self.gen,
|
||||
items: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, Self::Error> {
|
||||
Ok(SerializeTuple {
|
||||
gen: self.gen,
|
||||
items: Vec::with_capacity(len),
|
||||
title: "",
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize_tuple_struct(
|
||||
self,
|
||||
name: &'static str,
|
||||
len: usize,
|
||||
) -> Result<Self::SerializeTupleStruct, Self::Error> {
|
||||
let title = if self.include_title { name } else { "" };
|
||||
Ok(SerializeTuple {
|
||||
gen: self.gen,
|
||||
items: Vec::with_capacity(len),
|
||||
title,
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize_tuple_variant(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_variant_index: u32,
|
||||
_variant: &'static str,
|
||||
_len: usize,
|
||||
) -> Result<Self::SerializeTupleVariant, Self::Error> {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
|
||||
Ok(SerializeMap {
|
||||
gen: self.gen,
|
||||
properties: Map::new(),
|
||||
current_key: None,
|
||||
title: "",
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize_struct(
|
||||
self,
|
||||
name: &'static str,
|
||||
_len: usize,
|
||||
) -> Result<Self::SerializeStruct, Self::Error> {
|
||||
let title = if self.include_title { name } else { "" };
|
||||
Ok(SerializeMap {
|
||||
gen: self.gen,
|
||||
properties: Map::new(),
|
||||
current_key: None,
|
||||
title,
|
||||
})
|
||||
}
|
||||
|
||||
fn serialize_struct_variant(
|
||||
self,
|
||||
_name: &'static str,
|
||||
_variant_index: u32,
|
||||
_variant: &'static str,
|
||||
_len: usize,
|
||||
) -> Result<Self::SerializeStructVariant, Self::Error> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeTupleVariant for Serializer<'_> {
|
||||
type Ok = Schema;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_field<T: ?Sized>(&mut self, _value: &T) -> Result<(), Self::Error>
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Self::Error> {
|
||||
Ok(Schema::Bool(true))
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeStructVariant for Serializer<'_> {
|
||||
type Ok = Schema;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_field<T: ?Sized>(
|
||||
&mut self,
|
||||
_key: &'static str,
|
||||
_value: &T,
|
||||
) -> Result<(), Self::Error>
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Self::Error> {
|
||||
Ok(Schema::Bool(true))
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeSeq for SerializeSeq<'_> {
|
||||
type Ok = Schema;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_element<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error>
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
if self.items != Some(Schema::Bool(true)) {
|
||||
let schema = value.serialize(Serializer {
|
||||
gen: self.gen,
|
||||
include_title: false,
|
||||
})?;
|
||||
match &self.items {
|
||||
None => self.items = Some(schema),
|
||||
Some(items) => {
|
||||
if items != &schema {
|
||||
self.items = Some(Schema::Bool(true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Self::Error> {
|
||||
let items = self.items.unwrap_or(Schema::Bool(true));
|
||||
Ok(SchemaObject {
|
||||
instance_type: Some(InstanceType::Array.into()),
|
||||
array: Some(Box::new(ArrayValidation {
|
||||
items: Some(items.into()),
|
||||
..ArrayValidation::default()
|
||||
})),
|
||||
..SchemaObject::default()
|
||||
}
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeTuple for SerializeTuple<'_> {
|
||||
type Ok = Schema;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_element<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error>
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
let schema = value.serialize(Serializer {
|
||||
gen: self.gen,
|
||||
include_title: false,
|
||||
})?;
|
||||
self.items.push(schema);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Self::Error> {
|
||||
let len = self.items.len().try_into().ok();
|
||||
let mut schema = SchemaObject {
|
||||
instance_type: Some(InstanceType::Array.into()),
|
||||
array: Some(Box::new(ArrayValidation {
|
||||
items: Some(SingleOrVec::Vec(self.items)),
|
||||
max_items: len,
|
||||
min_items: len,
|
||||
..ArrayValidation::default()
|
||||
})),
|
||||
..SchemaObject::default()
|
||||
};
|
||||
|
||||
if !self.title.is_empty() {
|
||||
schema.metadata().title = Some(self.title.to_owned());
|
||||
}
|
||||
|
||||
Ok(schema.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeTupleStruct for SerializeTuple<'_> {
|
||||
type Ok = Schema;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_field<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error>
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
serde::ser::SerializeTuple::serialize_element(self, value)
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Self::Error> {
|
||||
serde::ser::SerializeTuple::end(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeMap for SerializeMap<'_> {
|
||||
type Ok = Schema;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_key<T: ?Sized>(&mut self, key: &T) -> Result<(), Self::Error>
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
// FIXME this is too lenient - we should return an error if serde_json
|
||||
// doesn't allow T to be a key of a map.
|
||||
let json = serde_json::to_string(key)?;
|
||||
self.current_key = Some(
|
||||
json.trim_start_matches('"')
|
||||
.trim_end_matches('"')
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_value<T: ?Sized>(&mut self, value: &T) -> Result<(), Self::Error>
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
let key = self.current_key.take().unwrap_or_default();
|
||||
let schema = value.serialize(Serializer {
|
||||
gen: self.gen,
|
||||
include_title: false,
|
||||
})?;
|
||||
self.properties.insert(key, schema);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Self::Error> {
|
||||
let mut schema = SchemaObject {
|
||||
instance_type: Some(InstanceType::Object.into()),
|
||||
object: Some(Box::new(ObjectValidation {
|
||||
properties: self.properties,
|
||||
..ObjectValidation::default()
|
||||
})),
|
||||
..SchemaObject::default()
|
||||
};
|
||||
|
||||
if !self.title.is_empty() {
|
||||
schema.metadata().title = Some(self.title.to_owned());
|
||||
}
|
||||
|
||||
Ok(schema.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeStruct for SerializeMap<'_> {
|
||||
type Ok = Schema;
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_field<T: ?Sized>(
|
||||
&mut self,
|
||||
key: &'static str,
|
||||
value: &T,
|
||||
) -> Result<(), Self::Error>
|
||||
where
|
||||
T: serde::Serialize,
|
||||
{
|
||||
let prop_schema = value.serialize(Serializer {
|
||||
gen: self.gen,
|
||||
include_title: false,
|
||||
})?;
|
||||
self.properties.insert(key.to_string(), prop_schema);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Self::Error> {
|
||||
serde::ser::SerializeMap::end(self)
|
||||
}
|
||||
}
|
80
schemars/tests/expected/from_value_2019_09.json
Normal file
80
schemars/tests/expected/from_value_2019_09.json
Normal file
|
@ -0,0 +1,80 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||
"title": "MyStruct",
|
||||
"examples": [
|
||||
{
|
||||
"myBool": true,
|
||||
"myInnerStruct": {
|
||||
"my_empty_map": {},
|
||||
"my_empty_vec": [],
|
||||
"my_map": {
|
||||
"": 0.0
|
||||
},
|
||||
"my_tuple": [
|
||||
"💩",
|
||||
42
|
||||
],
|
||||
"my_vec": [
|
||||
"hello",
|
||||
"world"
|
||||
]
|
||||
},
|
||||
"myInt": 123,
|
||||
"myNullableEnum": null
|
||||
}
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"myInt": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"myBool": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"myNullableEnum": true,
|
||||
"myInnerStruct": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"my_map": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
}
|
||||
},
|
||||
"my_vec": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"my_empty_map": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"my_empty_vec": {
|
||||
"type": "array",
|
||||
"items": true
|
||||
},
|
||||
"my_tuple": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "string",
|
||||
"maxLength": 1,
|
||||
"minLength": 1
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
80
schemars/tests/expected/from_value_draft07.json
Normal file
80
schemars/tests/expected/from_value_draft07.json
Normal file
|
@ -0,0 +1,80 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "MyStruct",
|
||||
"examples": [
|
||||
{
|
||||
"myBool": true,
|
||||
"myInnerStruct": {
|
||||
"my_empty_map": {},
|
||||
"my_empty_vec": [],
|
||||
"my_map": {
|
||||
"": 0.0
|
||||
},
|
||||
"my_tuple": [
|
||||
"💩",
|
||||
42
|
||||
],
|
||||
"my_vec": [
|
||||
"hello",
|
||||
"world"
|
||||
]
|
||||
},
|
||||
"myInt": 123,
|
||||
"myNullableEnum": null
|
||||
}
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"myInt": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"myBool": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"myNullableEnum": true,
|
||||
"myInnerStruct": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"my_map": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
}
|
||||
},
|
||||
"my_vec": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"my_empty_map": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"my_empty_vec": {
|
||||
"type": "array",
|
||||
"items": true
|
||||
},
|
||||
"my_tuple": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "string",
|
||||
"maxLength": 1,
|
||||
"minLength": 1
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
80
schemars/tests/expected/from_value_openapi3.json
Normal file
80
schemars/tests/expected/from_value_openapi3.json
Normal file
|
@ -0,0 +1,80 @@
|
|||
{
|
||||
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
|
||||
"title": "MyStruct",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"myInt": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"myBool": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"myNullableEnum": {
|
||||
"nullable": true
|
||||
},
|
||||
"myInnerStruct": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"my_map": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
}
|
||||
},
|
||||
"my_vec": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"my_empty_map": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"my_empty_vec": {
|
||||
"type": "array",
|
||||
"items": {}
|
||||
},
|
||||
"my_tuple": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "string",
|
||||
"maxLength": 1,
|
||||
"minLength": 1
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
],
|
||||
"maxItems": 2,
|
||||
"minItems": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"myBool": true,
|
||||
"myInnerStruct": {
|
||||
"my_empty_map": {},
|
||||
"my_empty_vec": [],
|
||||
"my_map": {
|
||||
"": 0.0
|
||||
},
|
||||
"my_tuple": [
|
||||
"💩",
|
||||
42
|
||||
],
|
||||
"my_vec": [
|
||||
"hello",
|
||||
"world"
|
||||
]
|
||||
},
|
||||
"myInt": 123,
|
||||
"myNullableEnum": null
|
||||
}
|
||||
}
|
77
schemars/tests/from_value.rs
Normal file
77
schemars/tests/from_value.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
mod util;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use schemars::gen::SchemaSettings;
|
||||
use serde::Serialize;
|
||||
use util::*;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MyStruct {
|
||||
pub my_int: i32,
|
||||
pub my_bool: bool,
|
||||
pub my_nullable_enum: Option<MyEnum>,
|
||||
pub my_inner_struct: MyInnerStruct,
|
||||
#[serde(skip)]
|
||||
pub skip: i32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub skip_if_none: Option<MyEnum>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct MyInnerStruct {
|
||||
pub my_map: HashMap<String, f64>,
|
||||
pub my_vec: Vec<&'static str>,
|
||||
pub my_empty_map: HashMap<String, f64>,
|
||||
pub my_empty_vec: Vec<&'static str>,
|
||||
pub my_tuple: (char, u8),
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub enum MyEnum {
|
||||
StringNewType(String),
|
||||
StructVariant { floats: Vec<f32> },
|
||||
}
|
||||
|
||||
fn make_value() -> MyStruct {
|
||||
let mut value = MyStruct {
|
||||
my_int: 123,
|
||||
my_bool: true,
|
||||
my_nullable_enum: None,
|
||||
my_inner_struct: MyInnerStruct {
|
||||
my_map: HashMap::new(),
|
||||
my_vec: vec!["hello", "world"],
|
||||
my_empty_map: HashMap::new(),
|
||||
my_empty_vec: vec![],
|
||||
my_tuple: ('💩', 42),
|
||||
},
|
||||
skip: 123,
|
||||
skip_if_none: None,
|
||||
};
|
||||
value.my_inner_struct.my_map.insert(String::new(), 0.0);
|
||||
value
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_from_value_matches_draft07() -> TestResult {
|
||||
let gen = SchemaSettings::draft07().into_generator();
|
||||
let actual = gen.into_root_schema_for_value(&make_value())?;
|
||||
|
||||
test_schema(&actual, "from_value_draft07")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_from_value_matches_2019_09() -> TestResult {
|
||||
let gen = SchemaSettings::draft2019_09().into_generator();
|
||||
let actual = gen.into_root_schema_for_value(&make_value())?;
|
||||
|
||||
test_schema(&actual, "from_value_2019_09")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_from_value_matches_openapi3() -> TestResult {
|
||||
let gen = SchemaSettings::openapi3().into_generator();
|
||||
let actual = gen.into_root_schema_for_value(&make_value())?;
|
||||
|
||||
test_schema(&actual, "from_value_openapi3")
|
||||
}
|
5
schemars/tests/ui/schema_for_arg_value.rs
Normal file
5
schemars/tests/ui/schema_for_arg_value.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
use schemars::schema_for;
|
||||
|
||||
fn main() {
|
||||
let _schema = schema_for!(123);
|
||||
}
|
7
schemars/tests/ui/schema_for_arg_value.stderr
Normal file
7
schemars/tests/ui/schema_for_arg_value.stderr
Normal file
|
@ -0,0 +1,7 @@
|
|||
error: This argument to `schema_for!` is not a type - did you mean to use `schema_for_value!` instead?
|
||||
--> $DIR/schema_for_arg_value.rs:4:19
|
||||
|
|
||||
4 | let _schema = schema_for!(123);
|
||||
| ^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
|
|
@ -18,7 +18,7 @@ pub fn test_default_generated_schema<T: JsonSchema>(file: &str) -> TestResult {
|
|||
test_schema(&actual, file)
|
||||
}
|
||||
|
||||
fn test_schema(actual: &RootSchema, file: &str) -> TestResult {
|
||||
pub fn test_schema(actual: &RootSchema, file: &str) -> TestResult {
|
||||
let expected_json = match fs::read_to_string(format!("tests/expected/{}.json", file)) {
|
||||
Ok(j) => j,
|
||||
Err(e) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue