Generate schema from any serializable value (#75)

Implement schema_for_value!(...) macro
This commit is contained in:
Graham Esau 2021-03-25 18:32:28 +00:00 committed by GitHub
parent 0957204bc1
commit f6482fd460
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1179 additions and 5 deletions

View file

@ -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. `#[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 ## Feature Flags
- `derive` (enabled by default) - provides `#[derive(JsonSchema)]` macro - `derive` (enabled by default) - provides `#[derive(JsonSchema)]` macro
- `impl_json_schema` - implements `JsonSchema` for Schemars types themselves - `impl_json_schema` - implements `JsonSchema` for Schemars types themselves

View file

@ -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. See the API documentation for more info on how to use those types for custom schema generation.
<!-- TODO: <!-- TODO:
create and link to example create and link to example
Generating schema from example value
--> -->

View 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());
}

View 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
}
}

View 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());
}

View 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
}
}

View file

@ -2,6 +2,9 @@ use crate::schema::*;
use crate::{Map, Set}; use crate::{Map, Set};
impl Schema { impl Schema {
/// This function is only public for use by schemars_derive.
///
/// It should not be considered part of the public API.
#[doc(hidden)] #[doc(hidden)]
pub fn flatten(self, other: Self) -> Schema { pub fn flatten(self, other: Self) -> Schema {
if is_null_type(&self) { if is_null_type(&self) {

View file

@ -1,7 +1,7 @@
/*! /*!
JSON Schema generator and settings. 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: 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). * [`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. * [`SchemaGenerator`], which manages the generation of a schema document.
@ -11,6 +11,7 @@ use crate::flatten::Merge;
use crate::schema::*; use crate::schema::*;
use crate::{visit::*, JsonSchema, Map}; use crate::{visit::*, JsonSchema, Map};
use dyn_clone::DynClone; use dyn_clone::DynClone;
use serde::Serialize;
use std::{any::Any, collections::HashSet, fmt::Debug}; use std::{any::Any, collections::HashSet, fmt::Debug};
/// Settings to customize how Schemas are generated. /// Settings to customize how Schemas are generated.
@ -314,6 +315,70 @@ impl SchemaGenerator {
root 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. /// 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 /// If the given `schema` has a [`$ref`](../schema/struct.SchemaObject.html#structfield.reference) property which refers

View file

@ -25,6 +25,7 @@ impl<T: JsonSchema> JsonSchema for Option<T> {
schema schema
} }
schema => SchemaObject { schema => SchemaObject {
// TODO technically the schema already accepts null, so this may be unnecessary
subschemas: Some(Box::new(SubschemaValidation { subschemas: Some(Box::new(SubschemaValidation {
any_of: Some(vec![schema, <()>::json_schema(gen)]), any_of: Some(vec![schema, <()>::json_schema(gen)]),
..Default::default() ..Default::default()

View file

@ -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. `#[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 ## Feature Flags
- `derive` (enabled by default) - provides `#[derive(JsonSchema)]` macro. - `derive` (enabled by default) - provides `#[derive(JsonSchema)]` macro
- `impl_json_schema` - implements `JsonSchema` for Schemars types themselves. - `impl_json_schema` - implements `JsonSchema` for Schemars types themselves
- `preserve_order` - keep the order of struct fields in `Schema` and `SchemaObject` - `preserve_order` - keep the order of struct fields in `Schema` and `SchemaObject`
## Optional Dependencies ## Optional Dependencies
@ -236,6 +298,7 @@ pub type MapEntry<'a, K, V> = indexmap::map::Entry<'a, K, V>;
mod flatten; mod flatten;
mod json_schema_impls; mod json_schema_impls;
mod ser;
#[macro_use] #[macro_use]
mod macros; mod macros;

View file

@ -13,9 +13,66 @@
/// ///
/// let my_schema = schema_for!(MyStruct); /// let my_schema = schema_for!(MyStruct);
/// ``` /// ```
#[cfg(doc)]
#[macro_export] #[macro_export]
macro_rules! schema_for { macro_rules! schema_for {
($type:ty) => { ($type:ty) => {
$crate::gen::SchemaGenerator::default().into_root_schema_for::<$type>() $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
View 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)
}
}

View 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
}
}
}
}
}

View 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
}
}
}
}
}

View 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
}
}

View 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")
}

View file

@ -0,0 +1,5 @@
use schemars::schema_for;
fn main() {
let _schema = schema_for!(123);
}

View 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)

View file

@ -18,7 +18,7 @@ pub fn test_default_generated_schema<T: JsonSchema>(file: &str) -> TestResult {
test_schema(&actual, file) 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)) { let expected_json = match fs::read_to_string(format!("tests/expected/{}.json", file)) {
Ok(j) => j, Ok(j) => j,
Err(e) => { Err(e) => {