Define Schema as a newtype around serde_json::Value (#289)

This commit is contained in:
Graham Esau 2024-05-12 19:23:54 +01:00 committed by GitHub
parent 7f6a7b7e32
commit 342cd5fd09
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
79 changed files with 1410 additions and 2394 deletions

View file

@ -17,7 +17,7 @@ jobs:
- nightly
include:
- rust: 1.60.0
test_features: "--features impl_json_schema"
test_features: ""
allow_failure: false
- rust: stable
test_features: "--all-features"

100
Cargo.lock generated
View file

@ -2,12 +2,6 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "arrayvec"
version = "0.7.4"
@ -29,17 +23,6 @@ dependencies = [
"serde",
]
[[package]]
name = "bigdecimal"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "bigdecimal"
version = "0.4.2"
@ -168,12 +151,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.2"
@ -196,17 +173,6 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]]
name = "indexmap"
version = "2.0.2"
@ -214,8 +180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897"
dependencies = [
"equivalent",
"hashbrown 0.14.2",
"serde",
"hashbrown",
]
[[package]]
@ -284,29 +249,49 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.69"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.33"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ref-cast"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4846d4c50d1721b1a3bef8af76924eef20d5e723647333798c1b519b3a9473f"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "rust_decimal"
version = "1.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4c4216490d5a413bc6d10fa4742bd7d4955941d062c0ef873141d6b0e7b30fd"
dependencies = [
"arrayvec 0.7.4",
"arrayvec",
"num-traits",
]
@ -320,18 +305,16 @@ checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
name = "schemars"
version = "0.8.19"
dependencies = [
"arrayvec 0.5.2",
"arrayvec 0.7.4",
"bigdecimal 0.3.1",
"bigdecimal 0.4.2",
"arrayvec",
"bigdecimal",
"bytes",
"chrono",
"dyn-clone",
"either",
"enumset",
"indexmap 1.9.3",
"indexmap 2.0.2",
"indexmap",
"pretty_assertions",
"ref-cast",
"rust_decimal",
"schemars_derive",
"semver",
@ -341,8 +324,7 @@ dependencies = [
"smol_str",
"trybuild",
"url",
"uuid 0.8.2",
"uuid 1.5.0",
"uuid",
]
[[package]]
@ -361,9 +343,6 @@ name = "semver"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
dependencies = [
"serde",
]
[[package]]
name = "serde"
@ -415,18 +394,15 @@ checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a"
[[package]]
name = "smol_str"
version = "0.1.24"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9"
dependencies = [
"serde",
]
checksum = "e6845563ada680337a52d43bb0b29f396f2d911616f6573012645b9e3d048a49"
[[package]]
name = "syn"
version = "2.0.38"
version = "2.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
dependencies = [
"proc-macro2",
"quote",
@ -504,12 +480,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
[[package]]
name = "uuid"
version = "1.5.0"

View file

@ -15,117 +15,91 @@ rust-version = "1.60"
[dependencies]
schemars_derive = { version = "=0.8.19", optional = true, path = "../schemars_derive" }
serde = { version = "1.0", features = ["derive"] }
serde = "1.0"
serde_json = "1.0.25"
dyn-clone = "1.0"
ref-cast = "1.0.22"
chrono = { version = "0.4", default-features = false, optional = true }
indexmap = { version = "1.2", features = ["serde-1"], optional = true }
indexmap2 = { version = "2.0", features = ["serde"], optional = true, package = "indexmap" }
either = { version = "1.3", default-features = false, optional = true }
uuid08 = { version = "0.8", default-features = false, optional = true, package = "uuid" }
# optional dependencies
chrono04 = { version = "0.4", default-features = false, optional = true, package = "chrono" }
indexmap2 = { version = "2.0", default-features = false, optional = true, package = "indexmap" }
either1 = { version = "1.3", default-features = false, optional = true, package = "either" }
uuid1 = { version = "1.0", default-features = false, optional = true, package = "uuid" }
smallvec = { version = "1.0", optional = true }
arrayvec05 = { version = "0.5", default-features = false, optional = true, package = "arrayvec" }
smallvec1 = { version = "1.0", default-features = false, optional = true, package = "smallvec" }
arrayvec07 = { version = "0.7", default-features = false, optional = true, package = "arrayvec" }
url = { version = "2.0", default-features = false, optional = true }
bytes = { version = "1.0", optional = true }
rust_decimal = { version = "1", default-features = false, optional = true }
bigdecimal03 = { version = "0.3", default-features = false, optional = true, package = "bigdecimal" }
url2 = { version = "2.0", default-features = false, optional = true, package = "url" }
bytes1 = { version = "1.0", default-features = false, optional = true, package = "bytes" }
rust_decimal1 = { version = "1", default-features = false, optional = true, package = "rust_decimal"}
bigdecimal04 = { version = "0.4", default-features = false, optional = true, package = "bigdecimal" }
enumset = { version = "1.0", optional = true }
smol_str = { version = "0.1.17", optional = true }
semver = { version = "1.0.9", features = ["serde"], optional = true }
enumset1 = { version = "1.0", default-features = false, optional = true, package = "enumset" }
smol_str02 = { version = "0.2.1", default-features = false, optional = true, package = "smol_str" }
semver1 = { version = "1.0.9", default-features = false, optional = true, package = "semver" }
[dev-dependencies]
pretty_assertions = "1.2.1"
trybuild = "1.0"
serde = { version = "1.0", features = ["derive"] }
[features]
default = ["derive"]
derive = ["schemars_derive"]
# Use a different representation for the map type of Schemars.
# This allows data to be read into a Value and written back to a JSON string
# while preserving the order of map keys in the input.
preserve_order = ["indexmap"]
impl_json_schema = ["derive"]
# derive_json_schema will be removed in a later version
derive_json_schema = ["impl_json_schema"]
# `uuid` feature contains `uuid08` only for back-compat - will be changed to include uuid 1.0 instead in a later version
uuid = ["uuid08"]
# `arrayvec` feature without version suffix is included only for back-compat - will be removed in a later version
arrayvec = ["arrayvec05"]
indexmap1 = ["indexmap"]
raw_value = ["serde_json/raw_value"]
# `bigdecimal` feature without version suffix is included only for back-compat - will be removed in a later version
bigdecimal = ["bigdecimal03"]
ui_test = []
[[test]]
name = "chrono"
required-features = ["chrono"]
[[test]]
name = "indexmap"
required-features = ["indexmap"]
[[test]]
name = "indexmap2"
required-features = ["indexmap2"]
[[test]]
name = "either"
required-features = ["either"]
[[test]]
name = "uuid"
required-features = ["uuid08", "uuid1"]
[[test]]
name = "smallvec"
required-features = ["smallvec"]
[[test]]
name = "bytes"
required-features = ["bytes"]
[[test]]
name = "arrayvec"
required-features = ["arrayvec05", "arrayvec07"]
[[test]]
name = "schema_for_schema"
required-features = ["impl_json_schema"]
[[test]]
name = "ui"
required-features = ["ui_test"]
[[test]]
name = "chrono"
required-features = ["chrono04"]
[[test]]
name = "indexmap"
required-features = ["indexmap2"]
[[test]]
name = "either"
required-features = ["either1"]
[[test]]
name = "uuid"
required-features = ["uuid1"]
[[test]]
name = "smallvec"
required-features = ["smallvec1"]
[[test]]
name = "bytes"
required-features = ["bytes1"]
[[test]]
name = "arrayvec"
required-features = ["arrayvec07"]
[[test]]
name = "url"
required-features = ["url"]
required-features = ["url2"]
[[test]]
name = "enumset"
required-features = ["enumset"]
required-features = ["enumset1"]
[[test]]
name = "smol_str"
required-features = ["smol_str"]
required-features = ["smol_str02"]
[[test]]
name = "semver"
required-features = ["semver"]
required-features = ["semver1"]
[[test]]
name = "decimal"
required-features = ["rust_decimal", "bigdecimal03", "bigdecimal04"]
required-features = ["rust_decimal1", "bigdecimal04"]
[package.metadata.docs.rs]
all-features = true

View file

@ -1,4 +1,4 @@
use schemars::schema::{Schema, SchemaObject};
use schemars::Schema;
use schemars::{gen::SchemaGenerator, schema_for, JsonSchema};
use serde::{Deserialize, Serialize};
@ -21,9 +21,11 @@ pub struct MyStruct {
}
fn make_custom_schema(gen: &mut SchemaGenerator) -> Schema {
let mut schema: SchemaObject = <String>::json_schema(gen).into();
schema.format = Some("boolean".to_owned());
schema.into()
let mut schema = String::json_schema(gen);
schema
.ensure_object()
.insert("format".into(), "boolean".into());
schema
}
fn eight() -> i32 {

View file

@ -1,7 +1,8 @@
use crate::gen::SchemaGenerator;
use crate::schema::{InstanceType, ObjectValidation, Schema, SchemaObject};
use crate::{JsonSchema, Map, Set};
use crate::JsonSchema;
use crate::Schema;
use serde::Serialize;
use serde_json::Map;
use serde_json::Value;
// Helper for generating schemas for flattened `Option` fields.
@ -12,12 +13,8 @@ pub fn json_schema_for_flatten<T: ?Sized + JsonSchema>(
let mut schema = T::_schemars_private_non_optional_json_schema(gen);
if T::_schemars_private_is_option() && !required {
if let Schema::Object(SchemaObject {
object: Some(ref mut object_validation),
..
}) = schema
{
object_validation.required.clear();
if let Some(object) = schema.as_object_mut() {
object.remove("required");
}
}
@ -57,38 +54,22 @@ impl<T: Serialize> MaybeSerializeWrapper<T> {
/// Create a schema for a unit enum
pub fn new_unit_enum(variant: &str) -> Schema {
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(vec![variant.into()]),
..SchemaObject::default()
// TODO switch from single-valued "enum" to "const"
json_schema!({
"type": "string",
"enum": [variant],
})
}
/// Create a schema for an externally tagged enum
pub fn new_externally_tagged_enum(variant: &str, sub_schema: Schema) -> Schema {
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::Object.into()),
object: Some(Box::new(ObjectValidation {
properties: {
let mut props = Map::new();
props.insert(variant.to_owned(), sub_schema);
props
},
required: {
let mut required = Set::new();
required.insert(variant.to_owned());
required
},
// Externally tagged variants must prohibit additional
// properties irrespective of the disposition of
// `deny_unknown_fields`. If additional properties were allowed
// one could easily construct an object that validated against
// multiple variants since here it's the properties rather than
// the values of a property that distingish between variants.
additional_properties: Some(Box::new(false.into())),
..Default::default()
})),
..SchemaObject::default()
json_schema!({
"type": "object",
"properties": {
variant: sub_schema
},
"required": [variant],
"additionalProperties": false,
})
}
@ -98,74 +79,87 @@ pub fn new_internally_tagged_enum(
variant: &str,
deny_unknown_fields: bool,
) -> Schema {
let tag_schema = Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(vec![variant.into()]),
..Default::default()
// TODO switch from single-valued "enum" to "const"
let mut schema = json_schema!({
"type": "object",
"properties": {
tag_name: {
"type": "string",
"enum": [variant],
}
},
"required": [tag_name],
});
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::Object.into()),
object: Some(Box::new(ObjectValidation {
properties: {
let mut props = Map::new();
props.insert(tag_name.to_owned(), tag_schema);
props
},
required: {
let mut required = Set::new();
required.insert(tag_name.to_owned());
required
},
additional_properties: deny_unknown_fields.then(|| Box::new(false.into())),
..Default::default()
})),
..SchemaObject::default()
})
if deny_unknown_fields {
schema
.as_object_mut()
.unwrap()
.insert("additionalProperties".into(), false.into());
}
schema
}
pub fn insert_object_property<T: ?Sized + JsonSchema>(
obj: &mut ObjectValidation,
schema: &mut Schema,
key: &str,
has_default: bool,
required: bool,
schema: Schema,
sub_schema: Schema,
) {
obj.properties.insert(key.to_owned(), 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());
}
if required || !(has_default || T::_schemars_private_is_option()) {
obj.required.insert(key.to_owned());
if let Some(req) = obj
.entry("required")
.or_insert(Value::Array(Vec::new()))
.as_array_mut()
{
req.push(key.into());
}
}
}
pub mod metadata {
use crate::Schema;
use serde_json::Value;
pub fn insert_validation_property(
schema: &mut Schema,
required_type: &str,
key: &str,
value: impl Into<Value>,
) {
if schema.has_type(required_type) || (required_type == "number" && schema.has_type("integer")) {
schema.ensure_object().insert(key.to_owned(), value.into());
}
}
macro_rules! add_metadata_fn {
($method:ident, $name:ident, $ty:ty) => {
pub fn $method(schema: Schema, $name: impl Into<$ty>) -> Schema {
let value = $name.into();
if value == <$ty>::default() {
schema
} else {
let mut schema_obj = schema.into_object();
schema_obj.metadata().$name = value.into();
Schema::Object(schema_obj)
}
pub fn append_required(schema: &mut Schema, key: &str) {
if schema.has_type("object") {
if let Value::Array(array) = schema
.ensure_object()
.entry("required")
.or_insert(Value::Array(Vec::new()))
{
let value = Value::from(key);
if !array.contains(&value) {
array.push(value);
}
};
}
add_metadata_fn!(add_description, description, String);
add_metadata_fn!(add_id, id, String);
add_metadata_fn!(add_title, title, String);
add_metadata_fn!(add_deprecated, deprecated, bool);
add_metadata_fn!(add_read_only, read_only, bool);
add_metadata_fn!(add_write_only, write_only, bool);
add_metadata_fn!(add_default, default, Value);
pub fn add_examples<I: IntoIterator<Item = Value>>(schema: Schema, examples: I) -> Schema {
let mut schema_obj = schema.into_object();
schema_obj.metadata().examples.extend(examples);
Schema::Object(schema_obj)
}
}
}
pub fn apply_inner_validation(schema: &mut Schema, f: fn(&mut Schema) -> ()) {
if let Some(inner_schema) = schema
.as_object_mut()
.and_then(|o| o.get_mut("items"))
.and_then(|i| <&mut Schema>::try_from(i).ok())
{
f(inner_schema);
}
}

View file

@ -1,180 +1,111 @@
use crate::schema::*;
use crate::{Map, Set};
use serde_json::map::Entry;
use serde_json::Value;
use crate::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)]
pub fn flatten(self, other: Self) -> Schema {
if is_null_type(&self) {
return other;
} else if is_null_type(&other) {
pub fn flatten(mut self, other: Self) -> Schema {
// This special null-type-schema handling is here for backward-compatibility, but needs reviewing.
// I think it's only needed to make internally-tagged enum unit variants behave correctly, but that
// should be handled entirely within schemars_derive.
if other
.as_object()
.and_then(|o| o.get("type"))
.and_then(|t| t.as_str())
== Some("null")
{
return self;
}
let s1: SchemaObject = self.into();
let s2: SchemaObject = other.into();
Schema::Object(s1.merge(s2))
}
}
pub(crate) trait Merge: Sized {
fn merge(self, other: Self) -> Self;
}
if let Value::Object(mut obj2) = other.to_value() {
let obj1 = self.ensure_object();
macro_rules! impl_merge {
($ty:ident { merge: $($merge_field:ident)*, or: $($or_field:ident)*, }) => {
impl Merge for $ty {
fn merge(self, other: Self) -> Self {
$ty {
$($merge_field: self.$merge_field.merge(other.$merge_field),)*
$($or_field: self.$or_field.or(other.$or_field),)*
let ap2 = obj2.remove("additionalProperties");
if let Entry::Occupied(mut ap1) = obj1.entry("additionalProperties") {
match ap2 {
Some(ap2) => {
flatten_additional_properties(ap1.get_mut(), ap2);
}
None => {
ap1.remove();
}
}
}
for (key, value2) in obj2 {
match obj1.entry(key) {
Entry::Vacant(vacant) => {
vacant.insert(value2);
}
Entry::Occupied(mut occupied) => {
match occupied.key().as_str() {
// This special "type" handling can probably be removed once the enum variant `with`/`schema_with` behaviour is fixed
"type" => match (occupied.get_mut(), value2) {
(Value::Array(a1), Value::Array(mut a2)) => {
a2.retain(|v2| !a1.contains(v2));
a1.extend(a2);
}
(v1, Value::Array(mut a2)) => {
if !a2.contains(v1) {
a2.push(std::mem::take(v1));
*occupied.get_mut() = Value::Array(a2);
}
}
(Value::Array(a1), v2) => {
if !a1.contains(&v2) {
a1.push(v2);
}
}
(v1, v2) => {
if v1 != &v2 {
*occupied.get_mut() =
Value::Array(vec![std::mem::take(v1), v2]);
}
}
},
"required" => {
if let Value::Array(a1) = occupied.into_mut() {
if let Value::Array(a2) = value2 {
a1.extend(a2);
}
}
}
"properties" | "patternProperties" => {
if let Value::Object(o1) = occupied.into_mut() {
if let Value::Object(o2) = value2 {
o1.extend(o2);
}
}
}
_ => {
// leave the original value as it is (don't modify `self`)
}
};
}
}
}
}
};
($ty:ident { or: $($or_field:ident)*, }) => {
impl_merge!( $ty { merge: , or: $($or_field)*, });
};
}
// For ObjectValidation::additional_properties.
impl Merge for Option<Box<Schema>> {
fn merge(self, other: Self) -> Self {
match (self.map(|x| *x), other.map(|x| *x)) {
// Perfer permissive schemas.
(Some(Schema::Bool(true)), _) => Some(Box::new(true.into())),
(_, Some(Schema::Bool(true))) => Some(Box::new(true.into())),
(None, _) => None,
(_, None) => None,
// Merge if we have two non-trivial schemas.
(Some(Schema::Object(s1)), Some(Schema::Object(s2))) => {
Some(Box::new(Schema::Object(s1.merge(s2))))
}
// Perfer the more permissive schema.
(Some(s1 @ Schema::Object(_)), Some(Schema::Bool(false))) => Some(Box::new(s1)),
(Some(Schema::Bool(false)), Some(s2 @ Schema::Object(_))) => Some(Box::new(s2)),
// Default to the null schema.
(Some(Schema::Bool(false)), Some(Schema::Bool(false))) => Some(Box::new(false.into())),
}
}
}
impl_merge!(SchemaObject {
merge: extensions instance_type enum_values
metadata subschemas number string array object,
or: format const_value reference,
});
impl Merge for Metadata {
fn merge(self, other: Self) -> Self {
Metadata {
id: self.id.or(other.id),
title: self.title.or(other.title),
description: self.description.or(other.description),
default: self.default.or(other.default),
deprecated: self.deprecated || other.deprecated,
read_only: self.read_only || other.read_only,
write_only: self.write_only || other.write_only,
examples: self.examples.merge(other.examples),
}
}
}
impl_merge!(SubschemaValidation {
or: all_of any_of one_of not if_schema then_schema else_schema,
});
impl_merge!(NumberValidation {
or: multiple_of maximum exclusive_maximum minimum exclusive_minimum,
});
impl_merge!(StringValidation {
or: max_length min_length pattern,
});
impl_merge!(ArrayValidation {
or: items additional_items max_items min_items unique_items contains,
});
impl_merge!(ObjectValidation {
merge: required properties pattern_properties additional_properties,
or: max_properties min_properties property_names,
});
impl<T: Merge> Merge for Option<T> {
fn merge(self, other: Self) -> Self {
match (self, other) {
(Some(x), Some(y)) => Some(x.merge(y)),
(None, y) => y,
(x, None) => x,
}
}
}
impl<T: Merge> Merge for Box<T> {
fn merge(mut self, other: Self) -> Self {
*self = (*self).merge(*other);
self
}
}
impl<T> Merge for Vec<T> {
fn merge(mut self, other: Self) -> Self {
self.extend(other);
self
}
}
impl<K, V> Merge for Map<K, V>
where
K: std::hash::Hash + Eq + Ord,
{
fn merge(mut self, other: Self) -> Self {
self.extend(other);
self
}
}
impl<T: Ord> Merge for Set<T> {
fn merge(mut self, other: Self) -> Self {
self.extend(other);
self
}
}
impl Merge for SingleOrVec<InstanceType> {
fn merge(self, other: Self) -> Self {
if self == other {
return self;
// TODO validate behaviour when flattening a normal struct into a struct with deny_unknown_fields
fn flatten_additional_properties(v1: &mut Value, v2: Value) {
match (v1, v2) {
(v1, Value::Bool(true)) => {
*v1 = Value::Bool(true);
}
let mut vec = match (self, other) {
(SingleOrVec::Vec(v1), SingleOrVec::Vec(v2)) => v1.merge(v2),
(SingleOrVec::Vec(mut v), SingleOrVec::Single(s))
| (SingleOrVec::Single(s), SingleOrVec::Vec(mut v)) => {
v.push(*s);
v
}
(SingleOrVec::Single(s1), SingleOrVec::Single(s2)) => vec![*s1, *s2],
};
vec.sort();
vec.dedup();
SingleOrVec::Vec(vec)
(v1 @ Value::Bool(false), v2) => {
*v1 = v2;
}
(Value::Object(o1), Value::Object(o2)) => {
o1.extend(o2);
}
_ => {}
}
}
fn is_null_type(schema: &Schema) -> bool {
let s = match schema {
Schema::Object(s) => s,
_ => return false,
};
let instance_type = match &s.instance_type {
Some(SingleOrVec::Single(t)) => t,
_ => return false,
};
**instance_type == InstanceType::Null
}

View file

@ -7,12 +7,12 @@ There are two main types in this module:
* [`SchemaGenerator`], which manages the generation of a schema document.
*/
use crate::schema::*;
use crate::{visit::*, JsonSchema, Map};
use crate::Schema;
use crate::{visit::*, JsonSchema};
use dyn_clone::DynClone;
use serde::Serialize;
use std::borrow::Cow;
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::{any::Any, collections::HashSet, fmt::Debug};
/// Settings to customize how Schemas are generated.
@ -76,7 +76,7 @@ impl SchemaSettings {
option_add_null_type: true,
definitions_path: "#/definitions/".to_owned(),
meta_schema: Some("https://json-schema.org/draft/2019-09/schema".to_owned()),
visitors: Vec::default(),
visitors: Vec::new(),
inline_subschemas: false,
}
}
@ -96,9 +96,7 @@ impl SchemaSettings {
Box::new(ReplaceBoolSchemas {
skip_additional_properties: true,
}),
Box::new(SetSingleExample {
retain_examples: false,
}),
Box::new(SetSingleExample),
],
inline_subschemas: false,
}
@ -150,7 +148,7 @@ impl SchemaSettings {
#[derive(Debug, Default)]
pub struct SchemaGenerator {
settings: SchemaSettings,
definitions: Map<String, Schema>,
definitions: BTreeMap<String, Schema>,
pending_schema_ids: HashSet<Cow<'static, str>>,
schema_id_to_name: HashMap<Cow<'static, str>, String>,
used_schema_names: HashSet<String>,
@ -198,19 +196,6 @@ impl SchemaGenerator {
&self.settings
}
#[deprecated = "This method no longer has any effect."]
pub fn make_extensible(&self, _schema: &mut SchemaObject) {}
#[deprecated = "Use `Schema::Bool(true)` instead"]
pub fn schema_for_any(&self) -> Schema {
Schema::Bool(true)
}
#[deprecated = "Use `Schema::Bool(false)` instead"]
pub fn schema_for_none(&self) -> Schema {
Schema::Bool(false)
}
/// Generates a JSON Schema for the type `T`, and returns either the schema itself or a `$ref` schema referencing `T`'s schema.
///
/// If `T` is [referenceable](JsonSchema::is_referenceable), this will add `T`'s schema to this generator's definitions, and
@ -262,7 +247,7 @@ impl SchemaGenerator {
name: String,
id: Cow<'static, str>,
) {
let dummy = Schema::Bool(false);
let dummy = false.into();
// insert into definitions BEFORE calling json_schema to avoid infinite recursion
self.definitions.insert(name.clone(), dummy);
@ -273,26 +258,26 @@ impl SchemaGenerator {
/// Borrows the collection of all [referenceable](JsonSchema::is_referenceable) schemas that have been generated.
///
/// The keys of the returned `Map` are the [schema names](JsonSchema::schema_name), and the values are the schemas
/// The keys of the returned `BTreeMap` are the [schema names](JsonSchema::schema_name), and the values are the schemas
/// themselves.
pub fn definitions(&self) -> &Map<String, Schema> {
pub fn definitions(&self) -> &BTreeMap<String, Schema> {
&self.definitions
}
/// Mutably borrows the collection of all [referenceable](JsonSchema::is_referenceable) schemas that have been generated.
///
/// The keys of the returned `Map` are the [schema names](JsonSchema::schema_name), and the values are the schemas
/// The keys of the returned `BTreeMap` are the [schema names](JsonSchema::schema_name), and the values are the schemas
/// themselves.
pub fn definitions_mut(&mut self) -> &mut Map<String, Schema> {
pub fn definitions_mut(&mut self) -> &mut BTreeMap<String, Schema> {
&mut self.definitions
}
/// Returns the collection of all [referenceable](JsonSchema::is_referenceable) schemas that have been generated,
/// leaving an empty map in its place.
///
/// The keys of the returned `Map` are the [schema names](JsonSchema::schema_name), and the values are the schemas
/// The keys of the returned `BTreeMap` are the [schema names](JsonSchema::schema_name), and the values are the schemas
/// themselves.
pub fn take_definitions(&mut self) -> Map<String, Schema> {
pub fn take_definitions(&mut self) -> BTreeMap<String, Schema> {
std::mem::take(&mut self.definitions)
}
@ -306,40 +291,72 @@ impl SchemaGenerator {
/// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will
/// add them to the `SchemaGenerator`'s schema definitions and include them in the returned `SchemaObject`'s
/// [`definitions`](../schema/struct.Metadata.html#structfield.definitions)
pub fn root_schema_for<T: ?Sized + JsonSchema>(&mut self) -> RootSchema {
let mut schema = self.json_schema_internal::<T>(T::schema_id()).into_object();
schema.metadata().title.get_or_insert_with(T::schema_name);
let mut root = RootSchema {
meta_schema: self.settings.meta_schema.clone(),
definitions: self.definitions.clone(),
schema,
};
pub fn root_schema_for<T: ?Sized + JsonSchema>(&mut self) -> Schema {
let mut schema = self.json_schema_internal::<T>(T::schema_id());
for visitor in &mut self.settings.visitors {
visitor.visit_root_schema(&mut root)
let object = schema.ensure_object();
object
.entry("title")
.or_insert_with(|| T::schema_name().into());
if let Some(meta_schema) = self.settings.meta_schema.as_deref() {
object.insert("$schema".into(), meta_schema.into());
}
root
if !self.definitions.is_empty() {
object.insert(
"definitions".into(),
serde_json::Value::Object(
self.definitions
.iter()
.map(|(k, v)| (k.clone(), v.clone().into()))
.collect(),
),
);
}
for visitor in &mut self.settings.visitors {
visitor.visit_schema(&mut schema);
}
schema
}
/// Consumes `self` and generates a root JSON Schema for the type `T`.
///
/// If `T`'s schema depends on any [referenceable](JsonSchema::is_referenceable) schemas, then this method will
/// include them in the returned `SchemaObject`'s [`definitions`](../schema/struct.Metadata.html#structfield.definitions)
pub fn into_root_schema_for<T: ?Sized + JsonSchema>(mut self) -> RootSchema {
let mut schema = self.json_schema_internal::<T>(T::schema_id()).into_object();
schema.metadata().title.get_or_insert_with(T::schema_name);
let mut root = RootSchema {
meta_schema: self.settings.meta_schema,
definitions: self.definitions,
schema,
};
pub fn into_root_schema_for<T: ?Sized + JsonSchema>(mut self) -> Schema {
let mut schema = self.json_schema_internal::<T>(T::schema_id());
for visitor in &mut self.settings.visitors {
visitor.visit_root_schema(&mut root)
let object = schema.ensure_object();
object
.entry("title")
.or_insert_with(|| T::schema_name().into());
if let Some(meta_schema) = self.settings.meta_schema {
object.insert("$schema".into(), meta_schema.into());
}
root
if !self.definitions.is_empty() {
object.insert(
"definitions".into(),
serde_json::Value::Object(
self.definitions
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect(),
),
);
}
for visitor in &mut self.settings.visitors {
visitor.visit_schema(&mut schema);
}
schema
}
/// Generates a root JSON Schema for the given example value.
@ -349,29 +366,39 @@ impl SchemaGenerator {
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();
) -> Result<Schema, serde_json::Error> {
let mut schema = value.serialize(crate::ser::Serializer {
gen: self,
include_title: true,
})?;
let object = schema.ensure_object();
if let Ok(example) = serde_json::to_value(value) {
schema.metadata().examples.push(example);
object.insert("examples".into(), vec![example].into());
}
let mut root = RootSchema {
meta_schema: self.settings.meta_schema.clone(),
definitions: self.definitions.clone(),
schema,
};
if let Some(meta_schema) = self.settings.meta_schema.as_deref() {
object.insert("$schema".into(), meta_schema.into());
}
if !self.definitions.is_empty() {
object.insert(
"definitions".into(),
serde_json::Value::Object(
self.definitions
.iter()
.map(|(k, v)| (k.clone(), v.clone().into()))
.collect(),
),
);
}
for visitor in &mut self.settings.visitors {
visitor.visit_root_schema(&mut root)
visitor.visit_schema(&mut schema);
}
Ok(root)
Ok(schema)
}
/// Consumes `self` and generates a root JSON Schema for the given example value.
@ -381,72 +408,39 @@ impl SchemaGenerator {
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();
) -> Result<Schema, serde_json::Error> {
let mut schema = value.serialize(crate::ser::Serializer {
gen: &mut self,
include_title: true,
})?;
let object = schema.ensure_object();
if let Ok(example) = serde_json::to_value(value) {
schema.metadata().examples.push(example);
object.insert("examples".into(), vec![example].into());
}
let mut root = RootSchema {
meta_schema: self.settings.meta_schema,
definitions: self.definitions,
schema,
};
if let Some(meta_schema) = self.settings.meta_schema {
object.insert("$schema".into(), meta_schema.into());
}
if !self.definitions.is_empty() {
object.insert(
"definitions".into(),
serde_json::Value::Object(
self.definitions
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect(),
),
);
}
for visitor in &mut self.settings.visitors {
visitor.visit_root_schema(&mut root)
visitor.visit_schema(&mut schema);
}
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
/// to another schema in `self`'s schema definitions, the referenced schema will be returned. Otherwise, returns `None`.
///
/// # Example
/// ```
/// use schemars::{JsonSchema, gen::SchemaGenerator};
///
/// #[derive(JsonSchema)]
/// struct MyStruct {
/// foo: i32,
/// }
///
/// let mut gen = SchemaGenerator::default();
/// let ref_schema = gen.subschema_for::<MyStruct>();
///
/// assert!(ref_schema.is_ref());
///
/// let dereferenced = gen.dereference(&ref_schema);
///
/// assert!(dereferenced.is_some());
/// assert!(!dereferenced.unwrap().is_ref());
/// assert_eq!(dereferenced, gen.definitions().get("MyStruct"));
/// ```
pub fn dereference<'a>(&'a self, schema: &Schema) -> Option<&'a Schema> {
match schema {
Schema::Object(SchemaObject {
reference: Some(ref schema_ref),
..
}) => {
let definitions_path = &self.settings().definitions_path;
if schema_ref.starts_with(definitions_path) {
let name = &schema_ref[definitions_path.len()..];
self.definitions.get(name)
} else {
None
}
}
_ => None,
}
Ok(schema)
}
fn json_schema_internal<T: ?Sized + JsonSchema>(&mut self, id: Cow<'static, str>) -> Schema {

View file

@ -1,6 +1,5 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use crate::{json_schema, JsonSchema, Schema};
use std::borrow::Cow;
// Does not require T: JsonSchema.
@ -16,15 +15,10 @@ impl<T> JsonSchema for [T; 0] {
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::Array.into()),
array: Some(Box::new(ArrayValidation {
max_items: Some(0),
..Default::default()
})),
..Default::default()
}
.into()
json_schema!({
"type": "array",
"maxItems": 0,
})
}
}
@ -44,17 +38,12 @@ macro_rules! array_impls {
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::Array.into()),
array: Some(Box::new(ArrayValidation {
items: Some(gen.subschema_for::<T>().into()),
max_items: Some($len),
min_items: Some($len),
..Default::default()
})),
..Default::default()
}
.into()
json_schema!({
"type": "array",
"items": serde_json::Value::from(gen.subschema_for::<T>()),
"minItems": $len,
"maxItems": $len,
})
}
}
)+
@ -67,40 +56,3 @@ array_impls! {
21 22 23 24 25 26 27 28 29 30
31 32
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{schema_for, schema_object_for};
use pretty_assertions::assert_eq;
#[test]
fn schema_for_array() {
let schema = schema_object_for::<[i32; 8]>();
assert_eq!(
schema.instance_type,
Some(SingleOrVec::from(InstanceType::Array))
);
let array_validation = schema.array.unwrap();
assert_eq!(
array_validation.items,
Some(SingleOrVec::from(schema_for::<i32>()))
);
assert_eq!(array_validation.max_items, Some(8));
assert_eq!(array_validation.min_items, Some(8));
}
// SomeStruct does not implement JsonSchema
struct SomeStruct;
#[test]
fn schema_for_empty_array() {
let schema = schema_object_for::<[SomeStruct; 0]>();
assert_eq!(
schema.instance_type,
Some(SingleOrVec::from(InstanceType::Array))
);
let array_validation = schema.array.unwrap();
assert_eq!(array_validation.max_items, Some(0));
}
}

View file

@ -1,37 +0,0 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use arrayvec05::{Array, ArrayString, ArrayVec};
use std::convert::TryInto;
// Do not set maxLength on the schema as that describes length in characters, but we only
// know max length in bytes.
forward_impl!((<A> JsonSchema for ArrayString<A> where A: Array<Item = u8> + Copy) => String);
impl<A: Array> JsonSchema for ArrayVec<A>
where
A::Item: JsonSchema,
{
no_ref_schema!();
fn schema_name() -> String {
format!(
"Array_up_to_size_{}_of_{}",
A::CAPACITY,
A::Item::schema_name()
)
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::Array.into()),
array: Some(Box::new(ArrayValidation {
items: Some(gen.subschema_for::<A::Item>().into()),
max_items: A::CAPACITY.try_into().ok(),
..Default::default()
})),
..Default::default()
}
.into()
}
}

View file

@ -1,8 +1,6 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use crate::{json_schema, JsonSchema, Schema};
use arrayvec07::{ArrayString, ArrayVec};
use std::convert::TryInto;
// Do not set maxLength on the schema as that describes length in characters, but we only
// know max length in bytes.
@ -19,15 +17,10 @@ where
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::Array.into()),
array: Some(Box::new(ArrayValidation {
items: Some(gen.subschema_for::<T>().into()),
max_items: CAP.try_into().ok(),
..Default::default()
})),
..Default::default()
}
.into()
json_schema!({
"type": "array",
"items": gen.subschema_for::<T>(),
"maxItems": CAP
})
}
}

View file

@ -1,6 +1,3 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use std::sync::atomic::*;
forward_impl!(AtomicBool => bool);
@ -22,12 +19,12 @@ forward_impl!(AtomicUsize => usize);
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::schema_object_for;
use crate::schema_for;
use pretty_assertions::assert_eq;
#[test]
fn schema_for_atomics() {
let atomic_schema = schema_object_for::<(
let atomic_schema = schema_for!((
AtomicBool,
AtomicI8,
AtomicI16,
@ -39,9 +36,8 @@ mod tests {
AtomicU32,
AtomicU64,
AtomicUsize,
)>();
let basic_schema =
schema_object_for::<(bool, i8, i16, i32, i64, isize, u8, u16, u32, u64, usize)>();
));
let basic_schema = schema_for!((bool, i8, i16, i32, i64, isize, u8, u16, u32, u64, usize));
assert_eq!(atomic_schema, basic_schema);
}
}

View file

@ -1,7 +0,0 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use bytes::{Bytes, BytesMut};
forward_impl!((JsonSchema for Bytes) => Vec<u8>);
forward_impl!((JsonSchema for BytesMut) => Vec<u8>);

View file

@ -1,8 +1,6 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use chrono::prelude::*;
use serde_json::json;
use crate::{json_schema, JsonSchema, Schema};
use chrono04::prelude::*;
use std::borrow::Cow;
impl JsonSchema for Weekday {
@ -17,20 +15,18 @@ impl JsonSchema for Weekday {
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(vec![
json!("Mon"),
json!("Tue"),
json!("Wed"),
json!("Thu"),
json!("Fri"),
json!("Sat"),
json!("Sun"),
]),
..Default::default()
}
.into()
json_schema!({
"type": "string",
"enum": [
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun",
]
})
}
}
@ -51,12 +47,10 @@ macro_rules! formatted_string_impl {
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::String.into()),
format: Some($format.to_owned()),
..Default::default()
}
.into()
json_schema!({
"type": "string",
"format": $format
})
}
}
};

View file

@ -1,7 +1,6 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use serde_json::json;
use crate::{json_schema, JsonSchema, Schema};
use serde_json::Value;
use std::borrow::Cow;
use std::ops::{Bound, Range, RangeInclusive};
@ -18,35 +17,45 @@ impl<T: JsonSchema> JsonSchema for Option<T> {
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let mut schema = gen.subschema_for::<T>();
if gen.settings().option_add_null_type {
schema = match schema {
Schema::Bool(true) => Schema::Bool(true),
Schema::Bool(false) => <()>::json_schema(gen),
Schema::Object(SchemaObject {
instance_type: Some(ref mut instance_type),
..
}) => {
add_null_type(instance_type);
schema
schema = match schema.try_to_object() {
Ok(mut obj) => {
let instance_type = obj.get_mut("type");
match instance_type {
Some(Value::Array(array)) => {
let null = Value::from("null");
if !array.contains(&null) {
array.push(null);
}
obj.into()
}
Some(Value::String(string)) => {
if string != "null" {
*instance_type.unwrap() =
Value::Array(vec![std::mem::take(string).into(), "null".into()])
}
obj.into()
}
_ => json_schema!({
"anyOf": [
obj,
<()>::json_schema(gen)
]
}),
}
}
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()
})),
..Default::default()
}
.into(),
Err(true) => true.into(),
Err(false) => <()>::json_schema(gen),
}
}
if gen.settings().option_nullable {
let mut schema_obj = schema.into_object();
schema_obj
.extensions
.insert("nullable".to_owned(), json!(true));
schema = Schema::Object(schema_obj);
schema
.ensure_object()
.insert("nullable".into(), true.into());
};
schema
}
@ -59,16 +68,6 @@ impl<T: JsonSchema> 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),
_ => {}
};
}
impl<T: JsonSchema, E: JsonSchema> JsonSchema for Result<T, E> {
fn schema_name() -> String {
format!("Result_of_{}_or_{}", T::schema_name(), E::schema_name())
@ -79,27 +78,24 @@ impl<T: JsonSchema, E: JsonSchema> JsonSchema for Result<T, E> {
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let mut ok_schema = SchemaObject {
instance_type: Some(InstanceType::Object.into()),
..Default::default()
};
let obj = ok_schema.object();
obj.required.insert("Ok".to_owned());
obj.properties
.insert("Ok".to_owned(), gen.subschema_for::<T>());
let mut err_schema = SchemaObject {
instance_type: Some(InstanceType::Object.into()),
..Default::default()
};
let obj = err_schema.object();
obj.required.insert("Err".to_owned());
obj.properties
.insert("Err".to_owned(), gen.subschema_for::<E>());
let mut schema = SchemaObject::default();
schema.subschemas().one_of = Some(vec![ok_schema.into(), err_schema.into()]);
schema.into()
json_schema!({
"oneOf": [
{
"type": "object",
"properties": {
"Ok": gen.subschema_for::<T>()
},
"required": ["Ok"]
},
{
"type": "object",
"properties": {
"Err": gen.subschema_for::<E>()
},
"required": ["Err"]
}
]
})
}
}
@ -113,37 +109,28 @@ impl<T: JsonSchema> JsonSchema for Bound<T> {
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let mut included_schema = SchemaObject {
instance_type: Some(InstanceType::Object.into()),
..Default::default()
};
let obj = included_schema.object();
obj.required.insert("Included".to_owned());
obj.properties
.insert("Included".to_owned(), gen.subschema_for::<T>());
let mut excluded_schema = SchemaObject {
instance_type: Some(InstanceType::Object.into()),
..Default::default()
};
let obj = excluded_schema.object();
obj.required.insert("Excluded".to_owned());
obj.properties
.insert("Excluded".to_owned(), gen.subschema_for::<T>());
let unbounded_schema = SchemaObject {
instance_type: Some(InstanceType::String.into()),
const_value: Some(json!("Unbounded")),
..Default::default()
};
let mut schema = SchemaObject::default();
schema.subschemas().one_of = Some(vec![
included_schema.into(),
excluded_schema.into(),
unbounded_schema.into(),
]);
schema.into()
json_schema!({
"oneOf": [
{
"type": "object",
"properties": {
"Included": gen.subschema_for::<T>()
},
"required": ["Included"]
},
{
"type": "object",
"properties": {
"Excluded": gen.subschema_for::<T>()
},
"required": ["Excluded"]
},
{
"type": "string",
"const": "Unbounded"
}
]
})
}
}
@ -157,18 +144,15 @@ impl<T: JsonSchema> JsonSchema for Range<T> {
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let mut schema = SchemaObject {
instance_type: Some(InstanceType::Object.into()),
..Default::default()
};
let obj = schema.object();
obj.required.insert("start".to_owned());
obj.required.insert("end".to_owned());
obj.properties
.insert("start".to_owned(), gen.subschema_for::<T>());
obj.properties
.insert("end".to_owned(), gen.subschema_for::<T>());
schema.into()
let subschema = gen.subschema_for::<T>();
json_schema!({
"type": "object",
"properties": {
"start": subschema,
"end": subschema
},
"required": ["start", "end"]
})
}
}
@ -177,54 +161,3 @@ forward_impl!((<T: JsonSchema> JsonSchema for RangeInclusive<T>) => Range<T>);
forward_impl!((<T: ?Sized> JsonSchema for std::marker::PhantomData<T>) => ());
forward_impl!((<'a> JsonSchema for std::fmt::Arguments<'a>) => String);
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{schema_for, schema_object_for};
use pretty_assertions::assert_eq;
#[test]
fn schema_for_option() {
let schema = schema_object_for::<Option<i32>>();
assert_eq!(
schema.instance_type,
Some(vec![InstanceType::Integer, InstanceType::Null].into())
);
assert_eq!(schema.extensions.get("nullable"), None);
assert_eq!(schema.subschemas.is_none(), true);
}
#[test]
fn schema_for_option_with_ref() {
use crate as schemars;
#[derive(JsonSchema)]
struct Foo;
let schema = schema_object_for::<Option<Foo>>();
assert_eq!(schema.instance_type, None);
assert_eq!(schema.extensions.get("nullable"), None);
assert_eq!(schema.subschemas.is_some(), true);
let any_of = schema.subschemas.unwrap().any_of.unwrap();
assert_eq!(any_of.len(), 2);
assert_eq!(any_of[0], Schema::new_ref("#/definitions/Foo".to_string()));
assert_eq!(any_of[1], schema_for::<()>());
}
#[test]
fn schema_for_result() {
let schema = schema_object_for::<Result<bool, String>>();
let one_of = schema.subschemas.unwrap().one_of.unwrap();
assert_eq!(one_of.len(), 2);
let ok_schema: SchemaObject = one_of[0].clone().into();
let obj = ok_schema.object.unwrap();
assert!(obj.required.contains("Ok"));
assert_eq!(obj.properties["Ok"], schema_for::<bool>());
let err_schema: SchemaObject = one_of[1].clone().into();
let obj = err_schema.object.unwrap();
assert!(obj.required.contains("Err"));
assert_eq!(obj.properties["Err"], schema_for::<String>());
}
}

View file

@ -1,6 +1,5 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use crate::{json_schema, JsonSchema, Schema};
use std::borrow::Cow;
macro_rules! decimal_impl {
@ -17,23 +16,16 @@ macro_rules! decimal_impl {
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::String.into()),
string: Some(Box::new(StringValidation {
pattern: Some(r"^-?[0-9]+(\.[0-9]+)?$".to_owned()),
..Default::default()
})),
..Default::default()
}
.into()
json_schema!({
"type": "string",
"pattern": r"^-?[0-9]+(\.[0-9]+)?$",
})
}
}
};
}
#[cfg(feature = "rust_decimal")]
decimal_impl!(rust_decimal::Decimal);
#[cfg(feature = "bigdecimal03")]
decimal_impl!(bigdecimal03::BigDecimal);
#[cfg(feature = "rust_decimal1")]
decimal_impl!(rust_decimal1::Decimal);
#[cfg(feature = "bigdecimal04")]
decimal_impl!(bigdecimal04::BigDecimal);

View file

@ -1,7 +1,6 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use either::Either;
use crate::{json_schema, JsonSchema, Schema};
use either1::Either;
use std::borrow::Cow;
impl<L: JsonSchema, R: JsonSchema> JsonSchema for Either<L, R> {
@ -20,8 +19,8 @@ impl<L: JsonSchema, R: JsonSchema> JsonSchema for Either<L, R> {
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let mut schema = SchemaObject::default();
schema.subschemas().any_of = Some(vec![gen.subschema_for::<L>(), gen.subschema_for::<R>()]);
schema.into()
json_schema!({
"anyOf": [gen.subschema_for::<L>(), gen.subschema_for::<R>()],
})
}
}

View file

@ -1,6 +0,0 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use enumset::{EnumSet, EnumSetType};
forward_impl!((<T> JsonSchema for EnumSet<T> where T: EnumSetType + JsonSchema) => std::collections::BTreeSet<T>);

View file

@ -1,6 +1,5 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use crate::{json_schema, JsonSchema, Schema};
use std::borrow::Cow;
use std::ffi::{CStr, CString, OsStr, OsString};
@ -14,27 +13,24 @@ impl JsonSchema for OsString {
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let mut unix_schema = SchemaObject {
instance_type: Some(InstanceType::Object.into()),
..Default::default()
};
let obj = unix_schema.object();
obj.required.insert("Unix".to_owned());
obj.properties
.insert("Unix".to_owned(), <Vec<u8>>::json_schema(gen));
let mut win_schema = SchemaObject {
instance_type: Some(InstanceType::Object.into()),
..Default::default()
};
let obj = win_schema.object();
obj.required.insert("Windows".to_owned());
obj.properties
.insert("Windows".to_owned(), <Vec<u16>>::json_schema(gen));
let mut schema = SchemaObject::default();
schema.subschemas().one_of = Some(vec![unix_schema.into(), win_schema.into()]);
schema.into()
json_schema!({
"oneOf": [
{
"type": "object",
"properties": {
"Unix": <Vec<u8>>::json_schema(gen)
},
"required": ["Unix"]
},
{
"type": "object",
"properties": {
"Windows": <Vec<u16>>::json_schema(gen)
},
"required": ["Windows"]
},
]
})
}
}

View file

@ -1,8 +0,0 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use indexmap::{IndexMap, IndexSet};
use std::collections::{HashMap, HashSet};
forward_impl!((<K, V: JsonSchema, H> JsonSchema for IndexMap<K, V, H>) => HashMap<K, V, H>);
forward_impl!((<T: JsonSchema, H> JsonSchema for IndexSet<T, H>) => HashSet<T, H>);

View file

@ -1,5 +1,3 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use indexmap2::{IndexMap, IndexSet};
use std::collections::{HashMap, HashSet};

View file

@ -1,6 +1,5 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use crate::{json_schema, JsonSchema, Schema};
use std::borrow::Cow;
macro_rules! map_impl {
@ -20,16 +19,10 @@ macro_rules! map_impl {
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let subschema = gen.subschema_for::<V>();
SchemaObject {
instance_type: Some(InstanceType::Object.into()),
object: Some(Box::new(ObjectValidation {
additional_properties: Some(Box::new(subschema)),
..Default::default()
})),
..Default::default()
}
.into()
json_schema!({
"type": "object",
"additionalProperties": gen.subschema_for::<V>(),
})
}
}
};

View file

@ -21,11 +21,11 @@ macro_rules! forward_impl {
<$target>::schema_id()
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
fn json_schema(gen: &mut $crate::gen::SchemaGenerator) -> $crate::Schema {
<$target>::json_schema(gen)
}
fn _schemars_private_non_optional_json_schema(gen: &mut SchemaGenerator) -> Schema {
fn _schemars_private_non_optional_json_schema(gen: &mut $crate::gen::SchemaGenerator) -> $crate::Schema {
<$target>::_schemars_private_non_optional_json_schema(gen)
}
@ -35,55 +35,61 @@ macro_rules! forward_impl {
}
};
($ty:ty => $target:ty) => {
forward_impl!((JsonSchema for $ty) => $target);
forward_impl!(($crate::JsonSchema for $ty) => $target);
};
}
mod array;
#[cfg(feature = "arrayvec05")]
mod arrayvec05;
#[cfg(feature = "arrayvec07")]
mod arrayvec07;
#[cfg(std_atomic)]
mod atomic;
#[cfg(feature = "bytes")]
mod bytes;
#[cfg(feature = "chrono")]
mod chrono;
mod core;
#[cfg(any(
feature = "rust_decimal",
feature = "bigdecimal03",
feature = "bigdecimal04"
))]
mod decimal;
#[cfg(feature = "either")]
mod either;
#[cfg(feature = "enumset")]
mod enumset;
mod ffi;
#[cfg(feature = "indexmap")]
mod indexmap;
#[cfg(feature = "indexmap2")]
mod indexmap2;
mod maps;
mod nonzero_signed;
mod nonzero_unsigned;
mod primitives;
#[cfg(feature = "semver")]
mod semver;
mod sequences;
mod serdejson;
#[cfg(feature = "smallvec")]
mod smallvec;
#[cfg(feature = "smol_str")]
mod smol_str;
mod time;
mod tuple;
#[cfg(feature = "url")]
mod url;
#[cfg(feature = "uuid08")]
mod uuid08;
mod wrapper;
#[cfg(std_atomic)]
mod atomic;
#[cfg(feature = "arrayvec07")]
mod arrayvec07;
#[cfg(feature = "bytes1")]
mod bytes1 {
forward_impl!(bytes1::Bytes => Vec<u8>);
forward_impl!(bytes1::BytesMut => Vec<u8>);
}
#[cfg(feature = "chrono04")]
mod chrono04;
#[cfg(any(feature = "rust_decimal1", feature = "bigdecimal04"))]
mod decimal;
#[cfg(feature = "either1")]
mod either1;
#[cfg(feature = "enumset1")]
forward_impl!((<T: enumset1::EnumSetType + crate::JsonSchema> crate::JsonSchema for enumset1::EnumSet<T>) => std::collections::BTreeSet<T>);
#[cfg(feature = "indexmap2")]
mod indexmap2;
#[cfg(feature = "semver1")]
mod semver1;
#[cfg(feature = "smallvec1")]
forward_impl!((<A: smallvec1::Array> crate::JsonSchema for smallvec1::SmallVec<A> where A::Item: crate::JsonSchema) => Vec<A::Item>);
#[cfg(feature = "smol_str02")]
forward_impl!(smol_str02::SmolStr => String);
#[cfg(feature = "url2")]
mod url2;
#[cfg(feature = "uuid1")]
mod uuid1;
mod wrapper;

View file

@ -1,6 +1,5 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use crate::{JsonSchema, Schema};
use std::borrow::Cow;
use std::num::*;
@ -18,14 +17,12 @@ macro_rules! nonzero_unsigned_impl {
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let zero_schema: Schema = SchemaObject {
const_value: Some(0.into()),
..Default::default()
}
.into();
let mut schema: SchemaObject = <$primitive>::json_schema(gen).into();
schema.subschemas().not = Some(Box::from(zero_schema));
schema.into()
let mut schema = <$primitive>::json_schema(gen);
let object = schema.ensure_object();
object.insert("not".to_owned(), serde_json::json!({
"const": 0
}));
schema
}
}
};

View file

@ -1,5 +1,5 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::Schema;
use crate::JsonSchema;
use std::borrow::Cow;
use std::num::*;
@ -18,9 +18,10 @@ macro_rules! nonzero_unsigned_impl {
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let mut schema: SchemaObject = <$primitive>::json_schema(gen).into();
schema.number().minimum = Some(1.0);
schema.into()
let mut schema = <$primitive>::json_schema(gen);
let object = schema.ensure_object();
object.insert("minimum".to_owned(), 1.into());
schema
}
}
};
@ -32,18 +33,3 @@ nonzero_unsigned_impl!(NonZeroU32 => u32);
nonzero_unsigned_impl!(NonZeroU64 => u64);
nonzero_unsigned_impl!(NonZeroU128 => u128);
nonzero_unsigned_impl!(NonZeroUsize => usize);
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::schema_object_for;
use pretty_assertions::assert_eq;
#[test]
fn schema_for_nonzero_u32() {
let schema = schema_object_for::<NonZeroU32>();
assert_eq!(schema.number.unwrap().minimum, Some(1.0));
assert_eq!(schema.instance_type, Some(InstanceType::Integer.into()));
assert_eq!(schema.format, Some("uint32".to_owned()));
}
}

View file

@ -1,67 +1,30 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use crate::{json_schema, JsonSchema, Schema};
use std::borrow::Cow;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
use std::path::{Path, PathBuf};
macro_rules! simple_impl {
($type:ty => $instance_type:ident) => {
simple_impl!($type => $instance_type, stringify!($instance_type), None);
};
($type:ty => $instance_type:ident, $format:literal) => {
simple_impl!($type => $instance_type, $format, Some($format.to_owned()));
};
($type:ty => $instance_type:ident, $name:expr, $format:expr) => {
($type:ty => $instance_type:literal) => {
impl JsonSchema for $type {
no_ref_schema!();
fn schema_name() -> String {
$name.to_owned()
$instance_type.to_owned()
}
fn schema_id() -> Cow<'static, str> {
Cow::Borrowed($name)
Cow::Borrowed($instance_type)
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::$instance_type.into()),
format: $format,
..Default::default()
}
.into()
json_schema!({
"type": $instance_type
})
}
}
};
}
simple_impl!(str => String);
simple_impl!(String => String);
simple_impl!(bool => Boolean);
simple_impl!(f32 => Number, "float");
simple_impl!(f64 => Number, "double");
simple_impl!(i8 => Integer, "int8");
simple_impl!(i16 => Integer, "int16");
simple_impl!(i32 => Integer, "int32");
simple_impl!(i64 => Integer, "int64");
simple_impl!(i128 => Integer, "int128");
simple_impl!(isize => Integer, "int");
simple_impl!(() => Null);
simple_impl!(Path => String);
simple_impl!(PathBuf => String);
simple_impl!(Ipv4Addr => String, "ipv4");
simple_impl!(Ipv6Addr => String, "ipv6");
simple_impl!(IpAddr => String, "ip");
simple_impl!(SocketAddr => String);
simple_impl!(SocketAddrV4 => String);
simple_impl!(SocketAddrV6 => String);
macro_rules! unsigned_impl {
($type:ty => $instance_type:ident, $format:expr) => {
($type:ty => $instance_type:literal, $format:literal) => {
impl JsonSchema for $type {
no_ref_schema!();
@ -74,24 +37,69 @@ macro_rules! unsigned_impl {
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
let mut schema = SchemaObject {
instance_type: Some(InstanceType::$instance_type.into()),
format: Some($format.to_owned()),
..Default::default()
};
schema.number().minimum = Some(0.0);
schema.into()
json_schema!({
"type": $instance_type,
"format": $format
})
}
}
};
}
unsigned_impl!(u8 => Integer, "uint8");
unsigned_impl!(u16 => Integer, "uint16");
unsigned_impl!(u32 => Integer, "uint32");
unsigned_impl!(u64 => Integer, "uint64");
unsigned_impl!(u128 => Integer, "uint128");
unsigned_impl!(usize => Integer, "uint");
simple_impl!(str => "string");
simple_impl!(String => "string");
simple_impl!(bool => "boolean");
simple_impl!(f32 => "number", "float");
simple_impl!(f64 => "number", "double");
simple_impl!(i8 => "integer", "int8");
simple_impl!(i16 => "integer", "int16");
simple_impl!(i32 => "integer", "int32");
simple_impl!(i64 => "integer", "int64");
simple_impl!(i128 => "integer", "int128");
simple_impl!(isize => "integer", "int");
simple_impl!(() => "null");
simple_impl!(Path => "string");
simple_impl!(PathBuf => "string");
simple_impl!(Ipv4Addr => "string", "ipv4");
simple_impl!(Ipv6Addr => "string", "ipv6");
simple_impl!(IpAddr => "string", "ip");
simple_impl!(SocketAddr => "string");
simple_impl!(SocketAddrV4 => "string");
simple_impl!(SocketAddrV6 => "string");
macro_rules! unsigned_impl {
($type:ty => $instance_type:literal, $format:literal) => {
impl JsonSchema for $type {
no_ref_schema!();
fn schema_name() -> String {
$format.to_owned()
}
fn schema_id() -> Cow<'static, str> {
Cow::Borrowed($format)
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
json_schema!({
"type": $instance_type,
"format": $format,
"minimum": 0
})
}
}
};
}
unsigned_impl!(u8 => "integer", "uint8");
unsigned_impl!(u16 => "integer", "uint16");
unsigned_impl!(u32 => "integer", "uint32");
unsigned_impl!(u64 => "integer", "uint64");
unsigned_impl!(u128 => "integer", "uint128");
unsigned_impl!(usize => "integer", "uint");
impl JsonSchema for char {
no_ref_schema!();
@ -105,15 +113,10 @@ impl JsonSchema for char {
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::String.into()),
string: Some(Box::new(StringValidation {
min_length: Some(1),
max_length: Some(1),
..Default::default()
})),
..Default::default()
}
.into()
json_schema!({
"type": "string",
"minLength": 1,
"maxLength": 1,
})
}
}

View file

@ -1,30 +0,0 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use semver::Version;
use std::borrow::Cow;
impl JsonSchema for Version {
no_ref_schema!();
fn schema_name() -> String {
"Version".to_owned()
}
fn schema_id() -> Cow<'static, str> {
Cow::Borrowed("semver::Version")
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::String.into()),
string: Some(Box::new(StringValidation {
// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
pattern: Some(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$".to_owned()),
..Default::default()
})),
..Default::default()
}
.into()
}
}

View file

@ -0,0 +1,24 @@
use crate::gen::SchemaGenerator;
use crate::{json_schema, JsonSchema, Schema};
use semver1::Version;
use std::borrow::Cow;
impl JsonSchema for Version {
no_ref_schema!();
fn schema_name() -> String {
"Version".to_owned()
}
fn schema_id() -> Cow<'static, str> {
Cow::Borrowed("semver::Version")
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
json_schema!({
"type": "string",
// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
"pattern": r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
})
}
}

View file

@ -1,6 +1,5 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use crate::{json_schema, JsonSchema, Schema};
use std::borrow::Cow;
macro_rules! seq_impl {
@ -21,15 +20,10 @@ macro_rules! seq_impl {
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::Array.into()),
array: Some(Box::new(ArrayValidation {
items: Some(gen.subschema_for::<T>().into()),
..Default::default()
})),
..Default::default()
}
.into()
json_schema!({
"type": "array",
"items": gen.subschema_for::<T>(),
})
}
}
};
@ -53,16 +47,11 @@ macro_rules! set_impl {
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::Array.into()),
array: Some(Box::new(ArrayValidation {
unique_items: Some(true),
items: Some(gen.subschema_for::<T>().into()),
..Default::default()
})),
..Default::default()
}
.into()
json_schema!({
"type": "array",
"uniqueItems": true,
"items": gen.subschema_for::<T>(),
})
}
}
};

View file

@ -1,6 +1,5 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use crate::{json_schema, JsonSchema, Schema};
use serde_json::{Map, Number, Value};
use std::borrow::Cow;
use std::collections::BTreeMap;
@ -17,7 +16,7 @@ impl JsonSchema for Value {
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
Schema::Bool(true)
true.into()
}
}
@ -35,11 +34,9 @@ impl JsonSchema for Number {
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::Number.into()),
..Default::default()
}
.into()
json_schema!({
"type": "number"
})
}
}

View file

@ -1,6 +0,0 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use smallvec::{Array, SmallVec};
forward_impl!((<A: Array> JsonSchema for SmallVec<A> where A::Item: JsonSchema) => Vec<A::Item>);

View file

@ -1,6 +0,0 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use smol_str::SmolStr;
forward_impl!(SmolStr => String);

View file

@ -1,6 +1,5 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use crate::{json_schema, JsonSchema, Schema};
use std::borrow::Cow;
use std::time::{Duration, SystemTime};
@ -14,18 +13,14 @@ impl JsonSchema for Duration {
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let mut schema = SchemaObject {
instance_type: Some(InstanceType::Object.into()),
..Default::default()
};
let obj = schema.object();
obj.required.insert("secs".to_owned());
obj.required.insert("nanos".to_owned());
obj.properties
.insert("secs".to_owned(), <u64>::json_schema(gen));
obj.properties
.insert("nanos".to_owned(), <u32>::json_schema(gen));
schema.into()
json_schema!({
"type": "object",
"required": ["secs", "nanos"],
"properties": {
"secs": u64::json_schema(gen),
"nanos": u32::json_schema(gen),
}
})
}
}
@ -39,17 +34,13 @@ impl JsonSchema for SystemTime {
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let mut schema = SchemaObject {
instance_type: Some(InstanceType::Object.into()),
..Default::default()
};
let obj = schema.object();
obj.required.insert("secs_since_epoch".to_owned());
obj.required.insert("nanos_since_epoch".to_owned());
obj.properties
.insert("secs_since_epoch".to_owned(), <u64>::json_schema(gen));
obj.properties
.insert("nanos_since_epoch".to_owned(), <u32>::json_schema(gen));
schema.into()
json_schema!({
"type": "object",
"required": ["secs_since_epoch", "nanos_since_epoch"],
"properties": {
"secs_since_epoch": u64::json_schema(gen),
"nanos_since_epoch": u32::json_schema(gen),
}
})
}
}

View file

@ -1,6 +1,5 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use crate::{json_schema, JsonSchema, Schema};
use std::borrow::Cow;
macro_rules! tuple_impls {
@ -24,20 +23,14 @@ macro_rules! tuple_impls {
}
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
let items = vec![
$(gen.subschema_for::<$name>()),+
];
SchemaObject {
instance_type: Some(InstanceType::Array.into()),
array: Some(Box::new(ArrayValidation {
items: Some(items.into()),
max_items: Some($len),
min_items: Some($len),
..Default::default()
})),
..Default::default()
}
.into()
json_schema!({
"type": "array",
"items": [
$(gen.subschema_for::<$name>()),+
],
"minItems": $len,
"maxItems": $len,
})
}
}
)+
@ -62,29 +55,3 @@ tuple_impls! {
15 => (T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 T12 T13 T14)
16 => (T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 T12 T13 T14 T15)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{schema_for, schema_object_for};
use pretty_assertions::assert_eq;
#[test]
fn schema_for_map_any_value() {
let schema = schema_object_for::<(i32, bool)>();
assert_eq!(
schema.instance_type,
Some(SingleOrVec::from(InstanceType::Array))
);
let array_validation = schema.array.unwrap();
assert_eq!(
array_validation.items,
Some(SingleOrVec::Vec(vec![
schema_for::<i32>(),
schema_for::<bool>()
]))
);
assert_eq!(array_validation.max_items, Some(2));
assert_eq!(array_validation.min_items, Some(2));
}
}

View file

@ -1,8 +1,7 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use crate::{json_schema, JsonSchema, Schema};
use std::borrow::Cow;
use url::Url;
use url2::Url;
impl JsonSchema for Url {
no_ref_schema!();
@ -16,11 +15,9 @@ impl JsonSchema for Url {
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::String.into()),
format: Some("uri".to_owned()),
..Default::default()
}
.into()
json_schema!({
"type": "string",
"format": "uri",
})
}
}

View file

@ -1,26 +0,0 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use std::borrow::Cow;
use uuid08::Uuid;
impl JsonSchema for Uuid {
no_ref_schema!();
fn schema_name() -> String {
"Uuid".to_string()
}
fn schema_id() -> Cow<'static, str> {
Cow::Borrowed("uuid::Uuid")
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::String.into()),
format: Some("uuid".to_string()),
..Default::default()
}
.into()
}
}

View file

@ -1,6 +1,5 @@
use crate::gen::SchemaGenerator;
use crate::schema::*;
use crate::JsonSchema;
use crate::{json_schema, JsonSchema, Schema};
use std::borrow::Cow;
use uuid1::Uuid;
@ -16,11 +15,9 @@ impl JsonSchema for Uuid {
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::String.into()),
format: Some("uuid".to_string()),
..Default::default()
}
.into()
json_schema!({
"type": "string",
"format": "uuid",
})
}
}

View file

@ -1,5 +1,3 @@
use crate::gen::SchemaGenerator;
use crate::schema::Schema;
use crate::JsonSchema;
macro_rules! wrapper_impl {

View file

@ -1,32 +1,9 @@
#![forbid(unsafe_code)]
#![deny(unsafe_code)]
#![doc = include_str!("../README.md")]
/// The map type used by schemars types.
///
/// Currently a `BTreeMap` or `IndexMap` can be used, but this may change to a different implementation
/// with a similar interface in a future version of schemars.
/// The `IndexMap` will be used when the `preserve_order` feature flag is set.
#[cfg(not(feature = "preserve_order"))]
pub type Map<K, V> = std::collections::BTreeMap<K, V>;
#[cfg(feature = "preserve_order")]
pub type Map<K, V> = indexmap::IndexMap<K, V>;
/// The set type used by schemars types.
///
/// Currently a `BTreeSet`, but this may change to a different implementation
/// with a similar interface in a future version of schemars.
pub type Set<T> = std::collections::BTreeSet<T>;
/// A view into a single entry in a map, which may either be vacant or occupied.
//
/// This is constructed from the `entry` method on `BTreeMap` or `IndexMap`,
/// depending on whether the `preserve_order` feature flag is set.
#[cfg(not(feature = "preserve_order"))]
pub type MapEntry<'a, K, V> = std::collections::btree_map::Entry<'a, K, V>;
#[cfg(feature = "preserve_order")]
pub type MapEntry<'a, K, V> = indexmap::map::Entry<'a, K, V>;
mod flatten;
mod json_schema_impls;
mod schema;
mod ser;
#[macro_use]
mod macros;
@ -36,7 +13,6 @@ mod macros;
#[doc(hidden)]
pub mod _private;
pub mod gen;
pub mod schema;
pub mod visit;
#[cfg(feature = "schemars_derive")]
@ -50,7 +26,7 @@ pub use schemars_derive::*;
#[doc(hidden)]
pub use serde_json as _serde_json;
use schema::Schema;
pub use schema::Schema;
/// A type which can be described as a JSON Schema document.
///
@ -75,7 +51,7 @@ use schema::Schema;
/// you will need to determine an appropriate name and ID for the type.
/// For non-generic types, the type name/path are suitable for this:
/// ```
/// use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema};
/// use schemars::{gen::SchemaGenerator, Schema, JsonSchema};
/// use std::borrow::Cow;
///
/// struct NonGenericType;
@ -101,7 +77,7 @@ use schema::Schema;
///
/// But generic type parameters which may affect the generated schema should typically be included in the name/ID:
/// ```
/// use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema};
/// use schemars::{gen::SchemaGenerator, Schema, JsonSchema};
/// use std::{borrow::Cow, marker::PhantomData};
///
/// struct GenericType<T>(PhantomData<T>);
@ -175,24 +151,3 @@ pub trait JsonSchema {
false
}
}
#[cfg(test)]
pub mod tests {
use super::*;
pub fn schema_object_for<T: JsonSchema>() -> schema::SchemaObject {
schema_object(schema_for::<T>())
}
pub fn schema_for<T: JsonSchema>() -> schema::Schema {
let mut gen = gen::SchemaGenerator::default();
T::json_schema(&mut gen)
}
pub fn schema_object(schema: schema::Schema) -> schema::SchemaObject {
match schema {
schema::Schema::Object(o) => o,
s => panic!("Schema was not an object: {:?}", s),
}
}
}

View file

@ -76,3 +76,19 @@ macro_rules! schema_for_value {
.unwrap()
};
}
// TODO doc
#[macro_export]
macro_rules! json_schema {
(
{$($json_object:tt)*}
) => {
$crate::Schema::try_from($crate::_serde_json::json!({$($json_object)*})).unwrap()
};
(true) => {
$crate::Schema::from(true)
};
(false) => {
$crate::Schema::from(false)
};
}

View file

@ -2,550 +2,229 @@
JSON Schema types.
*/
#[cfg(feature = "impl_json_schema")]
use crate as schemars;
#[cfg(feature = "impl_json_schema")]
use crate::JsonSchema;
use crate::{Map, Set};
use ref_cast::ref_cast_custom;
use ref_cast::RefCastCustom;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::ops::Deref;
use serde_json::{Map, Value};
/// A JSON Schema.
#[allow(clippy::large_enum_variant)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum Schema {
/// A trivial boolean JSON Schema.
///
/// The schema `true` matches everything (always passes validation), whereas the schema `false`
/// matches nothing (always fails validation).
Bool(bool),
/// A JSON Schema object.
Object(SchemaObject),
#[derive(Debug, Clone, PartialEq, RefCastCustom)]
#[repr(transparent)]
pub struct Schema(Value);
impl<'de> Deserialize<'de> for Schema {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
Schema::validate(&value)?;
Ok(Schema(value))
}
}
impl Serialize for Schema {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
ser::OrderedKeywordWrapper(&self.0).serialize(serializer)
}
}
impl Schema {
/// Creates a new `$ref` schema.
///
/// The given reference string should be a URI reference. This will usually be a JSON Pointer
/// in [URI Fragment representation](https://tools.ietf.org/html/rfc6901#section-6).
pub fn new_ref(reference: String) -> Self {
SchemaObject::new_ref(reference).into()
pub fn new() -> Self {
Self(Value::Object(Map::new()))
}
/// Returns `true` if `self` is a `$ref` schema.
///
/// If `self` is a [`SchemaObject`] with `Some` [`reference`](struct.SchemaObject.html#structfield.reference) set, this returns `true`.
/// Otherwise, returns `false`.
pub fn is_ref(&self) -> bool {
match self {
Schema::Object(o) => o.is_ref(),
pub fn new_ref(reference: String) -> Self {
let mut map = Map::new();
map.insert("$ref".to_owned(), Value::String(reference));
Self(Value::Object(map))
}
pub fn as_value(&self) -> &Value {
&self.0
}
pub fn as_bool(&self) -> Option<bool> {
self.0.as_bool()
}
pub fn as_object(&self) -> Option<&Map<String, Value>> {
self.0.as_object()
}
pub fn as_object_mut(&mut self) -> Option<&mut Map<String, Value>> {
self.0.as_object_mut()
}
pub(crate) fn try_to_object(self) -> Result<Map<String, Value>, bool> {
match self.0 {
Value::Object(m) => Ok(m),
Value::Bool(b) => Err(b),
_ => unreachable!(),
}
}
pub fn to_value(self) -> Value {
self.0
}
pub fn ensure_object(&mut self) -> &mut Map<String, Value> {
if let Some(b) = self.as_bool() {
let mut map = Map::new();
if !b {
map.insert("not".into(), Value::Object(Map::new()));
}
self.0 = Value::Object(map);
}
self.as_object_mut()
.expect("Schema value should be of type Object.")
}
pub(crate) fn has_type(&self, ty: &str) -> bool {
match self.0.get("type") {
Some(Value::Array(values)) => values.iter().any(|v| v.as_str() == Some(ty)),
Some(Value::String(s)) => s == ty,
_ => false,
}
}
/// Converts the given schema (if it is a boolean schema) into an equivalent schema object.
///
/// If the given schema is already a schema object, this has no effect.
///
/// # Example
/// ```
/// use schemars::schema::{Schema, SchemaObject};
///
/// let bool_schema = Schema::Bool(true);
///
/// assert_eq!(bool_schema.into_object(), SchemaObject::default());
/// ```
pub fn into_object(self) -> SchemaObject {
match self {
Schema::Object(o) => o,
Schema::Bool(true) => SchemaObject::default(),
Schema::Bool(false) => SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
not: Some(Schema::Object(Default::default()).into()),
..Default::default()
})),
..Default::default()
},
}
fn validate<E: serde::de::Error>(value: &Value) -> Result<(), E> {
use serde::de::Unexpected;
let unexpected = match value {
Value::Bool(_) | Value::Object(_) => return Ok(()),
Value::Null => Unexpected::Unit,
Value::Number(n) => {
if let Some(u) = n.as_u64() {
Unexpected::Unsigned(u)
} else if let Some(i) = n.as_i64() {
Unexpected::Signed(i)
} else if let Some(f) = n.as_f64() {
Unexpected::Float(f)
} else {
unreachable!()
}
}
Value::String(s) => Unexpected::Str(s),
Value::Array(_) => Unexpected::Seq,
};
Err(E::invalid_type(unexpected, &"object or boolean"))
}
#[allow(unsafe_code)]
#[ref_cast_custom]
fn ref_cast(value: &Value) -> &Self;
#[allow(unsafe_code)]
#[ref_cast_custom]
fn ref_cast_mut(value: &mut Value) -> &mut Self;
}
impl From<Schema> for Value {
fn from(v: Schema) -> Value {
v.0
}
}
impl From<SchemaObject> for Schema {
fn from(o: SchemaObject) -> Self {
Schema::Object(o)
impl std::convert::TryFrom<Value> for Schema {
type Error = serde_json::Error;
fn try_from(value: Value) -> serde_json::Result<Schema> {
Schema::validate(&value)?;
Ok(Schema(value))
}
}
impl<'a> std::convert::TryFrom<&'a Value> for &'a Schema {
type Error = serde_json::Error;
fn try_from(value: &Value) -> serde_json::Result<&Schema> {
Schema::validate(value)?;
Ok(Schema::ref_cast(value))
}
}
impl<'a> std::convert::TryFrom<&'a mut Value> for &'a mut Schema {
type Error = serde_json::Error;
fn try_from(value: &mut Value) -> serde_json::Result<&mut Schema> {
Schema::validate(value)?;
Ok(Schema::ref_cast_mut(value))
}
}
impl Default for Schema {
fn default() -> Self {
Self(Value::Object(Map::new()))
}
}
impl From<Map<String, Value>> for Schema {
fn from(o: Map<String, Value>) -> Self {
Schema(Value::Object(o))
}
}
impl From<bool> for Schema {
fn from(b: bool) -> Self {
Schema::Bool(b)
Schema(Value::Bool(b))
}
}
/// The root object of a JSON Schema document.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", default)]
pub struct RootSchema {
/// The `$schema` keyword.
///
/// See [JSON Schema 8.1.1. The "$schema" Keyword](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.1.1).
#[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
pub meta_schema: Option<String>,
/// The root schema itself.
#[serde(flatten)]
pub schema: SchemaObject,
/// The `definitions` keyword.
///
/// In JSON Schema draft 2019-09 this was replaced by $defs, but in Schemars this is still
/// serialized as `definitions` for backward-compatibility.
///
/// See [JSON Schema 8.2.5. Schema Re-Use With "$defs"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.5),
/// and [JSON Schema (draft 07) 9. Schema Re-Use With "definitions"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-01#section-9).
#[serde(alias = "$defs", skip_serializing_if = "Map::is_empty")]
pub definitions: Map<String, Schema>,
}
mod ser {
use serde::ser::{Serialize, SerializeMap, SerializeSeq};
use serde_json::Value;
/// A JSON Schema object.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", default)]
pub struct SchemaObject {
/// Properties which annotate the [`SchemaObject`] which typically have no effect when an object is being validated against the schema.
#[serde(flatten, deserialize_with = "skip_if_default")]
pub metadata: Option<Box<Metadata>>,
/// The `type` keyword.
///
/// See [JSON Schema Validation 6.1.1. "type"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.1)
/// and [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1).
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub instance_type: Option<SingleOrVec<InstanceType>>,
/// The `format` keyword.
///
/// See [JSON Schema Validation 7. A Vocabulary for Semantic Content With "format"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-7).
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
/// The `enum` keyword.
///
/// See [JSON Schema Validation 6.1.2. "enum"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.2)
#[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
pub enum_values: Option<Vec<Value>>,
/// The `const` keyword.
///
/// See [JSON Schema Validation 6.1.3. "const"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.1.3)
#[serde(
rename = "const",
skip_serializing_if = "Option::is_none",
deserialize_with = "allow_null"
)]
pub const_value: Option<Value>,
/// Properties of the [`SchemaObject`] which define validation assertions in terms of other schemas.
#[serde(flatten, deserialize_with = "skip_if_default")]
pub subschemas: Option<Box<SubschemaValidation>>,
/// Properties of the [`SchemaObject`] which define validation assertions for numbers.
#[serde(flatten, deserialize_with = "skip_if_default")]
pub number: Option<Box<NumberValidation>>,
/// Properties of the [`SchemaObject`] which define validation assertions for strings.
#[serde(flatten, deserialize_with = "skip_if_default")]
pub string: Option<Box<StringValidation>>,
/// Properties of the [`SchemaObject`] which define validation assertions for arrays.
#[serde(flatten, deserialize_with = "skip_if_default")]
pub array: Option<Box<ArrayValidation>>,
/// Properties of the [`SchemaObject`] which define validation assertions for objects.
#[serde(flatten, deserialize_with = "skip_if_default")]
pub object: Option<Box<ObjectValidation>>,
/// The `$ref` keyword.
///
/// See [JSON Schema 8.2.4.1. Direct References with "$ref"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.4.1).
#[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
pub reference: Option<String>,
/// Arbitrary extra properties which are not part of the JSON Schema specification, or which `schemars` does not support.
#[serde(flatten)]
pub extensions: Map<String, Value>,
}
const ORDERED_KEYWORDS_START: [&str; 6] =
["$id", "$schema", "title", "description", "type", "format"];
const ORDERED_KEYWORDS_END: [&str; 2] = ["$defs", "definitions"];
// Deserializing "null" to `Option<Value>` directly results in `None`,
// this function instead makes it deserialize to `Some(Value::Null)`.
fn allow_null<'de, D>(de: D) -> Result<Option<Value>, D::Error>
where
D: serde::Deserializer<'de>,
{
Value::deserialize(de).map(Option::Some)
}
pub(super) struct OrderedKeywordWrapper<'a>(pub &'a Value);
fn skip_if_default<'de, D, T>(deserializer: D) -> Result<Option<Box<T>>, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de> + Default + PartialEq,
{
let value = T::deserialize(deserializer)?;
if value == T::default() {
Ok(None)
} else {
Ok(Some(Box::new(value)))
}
}
impl Serialize for OrderedKeywordWrapper<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self.0 {
Value::Array(array) => {
let mut seq = serializer.serialize_seq(Some(array.len()))?;
for value in array {
seq.serialize_element(&OrderedKeywordWrapper(value))?;
}
seq.end()
}
Value::Object(object) => {
let mut map = serializer.serialize_map(Some(object.len()))?;
macro_rules! get_or_insert_default_fn {
($name:ident, $ret:ty) => {
get_or_insert_default_fn!(
concat!(
"Returns a mutable reference to this schema's [`",
stringify!($ret),
"`](#structfield.",
stringify!($name),
"), creating it if it was `None`."
),
$name,
$ret
);
};
($doc:expr, $name:ident, $ret:ty) => {
#[doc = $doc]
pub fn $name(&mut self) -> &mut $ret {
self.$name.get_or_insert_with(Default::default)
}
};
}
for key in ORDERED_KEYWORDS_START {
if let Some(value) = object.get(key) {
map.serialize_entry(key, &OrderedKeywordWrapper(value))?;
}
}
impl SchemaObject {
/// Creates a new `$ref` schema.
///
/// The given reference string should be a URI reference. This will usually be a JSON Pointer
/// in [URI Fragment representation](https://tools.ietf.org/html/rfc6901#section-6).
pub fn new_ref(reference: String) -> Self {
SchemaObject {
reference: Some(reference),
..Default::default()
}
}
for (key, value) in object {
if !ORDERED_KEYWORDS_START.contains(&key.as_str())
&& !ORDERED_KEYWORDS_END.contains(&key.as_str())
{
map.serialize_entry(key, &OrderedKeywordWrapper(value))?;
}
}
/// Returns `true` if `self` is a `$ref` schema.
///
/// If `self` has `Some` [`reference`](struct.SchemaObject.html#structfield.reference) set, this returns `true`.
/// Otherwise, returns `false`.
pub fn is_ref(&self) -> bool {
self.reference.is_some()
}
for key in ORDERED_KEYWORDS_END {
if let Some(value) = object.get(key) {
map.serialize_entry(key, &OrderedKeywordWrapper(value))?;
}
}
/// Returns `true` if `self` accepts values of the given type, according to the [`instance_type`](struct.SchemaObject.html#structfield.instance_type) field.
///
/// This is a basic check that always returns `true` if no `instance_type` is specified on the schema,
/// and does not check any subschemas. Because of this, both `{}` and `{"not": {}}` accept any type according
/// to this method.
pub fn has_type(&self, ty: InstanceType) -> bool {
self.instance_type
.as_ref()
.map_or(true, |x| x.contains(&ty))
}
get_or_insert_default_fn!(metadata, Metadata);
get_or_insert_default_fn!(subschemas, SubschemaValidation);
get_or_insert_default_fn!(number, NumberValidation);
get_or_insert_default_fn!(string, StringValidation);
get_or_insert_default_fn!(array, ArrayValidation);
get_or_insert_default_fn!(object, ObjectValidation);
}
impl From<Schema> for SchemaObject {
fn from(schema: Schema) -> Self {
schema.into_object()
}
}
/// Properties which annotate a [`SchemaObject`] which typically have no effect when an object is being validated against the schema.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", default)]
pub struct Metadata {
/// The `$id` keyword.
///
/// See [JSON Schema 8.2.2. The "$id" Keyword](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.2).
#[serde(rename = "$id", skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
/// The `title` keyword.
///
/// See [JSON Schema Validation 9.1. "title" and "description"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1).
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
/// The `description` keyword.
///
/// See [JSON Schema Validation 9.1. "title" and "description"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.1).
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// The `default` keyword.
///
/// See [JSON Schema Validation 9.2. "default"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.2).
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "allow_null"
)]
pub default: Option<Value>,
/// The `deprecated` keyword.
///
/// See [JSON Schema Validation 9.3. "deprecated"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.3).
#[serde(skip_serializing_if = "is_false")]
pub deprecated: bool,
/// The `readOnly` keyword.
///
/// See [JSON Schema Validation 9.4. "readOnly" and "writeOnly"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.4).
#[serde(skip_serializing_if = "is_false")]
pub read_only: bool,
/// The `writeOnly` keyword.
///
/// See [JSON Schema Validation 9.4. "readOnly" and "writeOnly"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.4).
#[serde(skip_serializing_if = "is_false")]
pub write_only: bool,
/// The `examples` keyword.
///
/// See [JSON Schema Validation 9.5. "examples"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-9.5).
#[serde(skip_serializing_if = "Vec::is_empty")]
pub examples: Vec<Value>,
}
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_false(b: &bool) -> bool {
!b
}
/// Properties of a [`SchemaObject`] which define validation assertions in terms of other schemas.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", default)]
pub struct SubschemaValidation {
/// The `allOf` keyword.
///
/// See [JSON Schema 9.2.1.1. "allOf"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.1).
#[serde(skip_serializing_if = "Option::is_none")]
pub all_of: Option<Vec<Schema>>,
/// The `anyOf` keyword.
///
/// See [JSON Schema 9.2.1.2. "anyOf"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.2).
#[serde(skip_serializing_if = "Option::is_none")]
pub any_of: Option<Vec<Schema>>,
/// The `oneOf` keyword.
///
/// See [JSON Schema 9.2.1.3. "oneOf"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.3).
#[serde(skip_serializing_if = "Option::is_none")]
pub one_of: Option<Vec<Schema>>,
/// The `not` keyword.
///
/// See [JSON Schema 9.2.1.4. "not"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.1.4).
#[serde(skip_serializing_if = "Option::is_none")]
pub not: Option<Box<Schema>>,
/// The `if` keyword.
///
/// See [JSON Schema 9.2.2.1. "if"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.1).
#[serde(rename = "if", skip_serializing_if = "Option::is_none")]
pub if_schema: Option<Box<Schema>>,
/// The `then` keyword.
///
/// See [JSON Schema 9.2.2.2. "then"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.2).
#[serde(rename = "then", skip_serializing_if = "Option::is_none")]
pub then_schema: Option<Box<Schema>>,
/// The `else` keyword.
///
/// See [JSON Schema 9.2.2.3. "else"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.2.2.3).
#[serde(rename = "else", skip_serializing_if = "Option::is_none")]
pub else_schema: Option<Box<Schema>>,
}
/// Properties of a [`SchemaObject`] which define validation assertions for numbers.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", default)]
pub struct NumberValidation {
/// The `multipleOf` keyword.
///
/// See [JSON Schema Validation 6.2.1. "multipleOf"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.1).
#[serde(skip_serializing_if = "Option::is_none")]
pub multiple_of: Option<f64>,
/// The `maximum` keyword.
///
/// See [JSON Schema Validation 6.2.2. "maximum"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.2).
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<f64>,
/// The `exclusiveMaximum` keyword.
///
/// See [JSON Schema Validation 6.2.3. "exclusiveMaximum"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.3).
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_maximum: Option<f64>,
/// The `minimum` keyword.
///
/// See [JSON Schema Validation 6.2.4. "minimum"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.4).
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<f64>,
/// The `exclusiveMinimum` keyword.
///
/// See [JSON Schema Validation 6.2.5. "exclusiveMinimum"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.2.5).
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_minimum: Option<f64>,
}
/// Properties of a [`SchemaObject`] which define validation assertions for strings.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", default)]
pub struct StringValidation {
/// The `maxLength` keyword.
///
/// See [JSON Schema Validation 6.3.1. "maxLength"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.1).
#[serde(skip_serializing_if = "Option::is_none")]
pub max_length: Option<u32>,
/// The `minLength` keyword.
///
/// See [JSON Schema Validation 6.3.2. "minLength"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.2).
#[serde(skip_serializing_if = "Option::is_none")]
pub min_length: Option<u32>,
/// The `pattern` keyword.
///
/// See [JSON Schema Validation 6.3.3. "pattern"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.3.3).
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
}
/// Properties of a [`SchemaObject`] which define validation assertions for arrays.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", default)]
pub struct ArrayValidation {
/// The `items` keyword.
///
/// See [JSON Schema 9.3.1.1. "items"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.1).
#[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<SingleOrVec<Schema>>,
/// The `additionalItems` keyword.
///
/// See [JSON Schema 9.3.1.2. "additionalItems"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.2).
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_items: Option<Box<Schema>>,
/// The `maxItems` keyword.
///
/// See [JSON Schema Validation 6.4.1. "maxItems"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.1).
#[serde(skip_serializing_if = "Option::is_none")]
pub max_items: Option<u32>,
/// The `minItems` keyword.
///
/// See [JSON Schema Validation 6.4.2. "minItems"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.2).
#[serde(skip_serializing_if = "Option::is_none")]
pub min_items: Option<u32>,
/// The `uniqueItems` keyword.
///
/// See [JSON Schema Validation 6.4.3. "uniqueItems"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.4.3).
#[serde(skip_serializing_if = "Option::is_none")]
pub unique_items: Option<bool>,
/// The `contains` keyword.
///
/// See [JSON Schema 9.3.1.4. "contains"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.1.4).
#[serde(skip_serializing_if = "Option::is_none")]
pub contains: Option<Box<Schema>>,
}
/// Properties of a [`SchemaObject`] which define validation assertions for objects.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase", default)]
pub struct ObjectValidation {
/// The `maxProperties` keyword.
///
/// See [JSON Schema Validation 6.5.1. "maxProperties"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.1).
#[serde(skip_serializing_if = "Option::is_none")]
pub max_properties: Option<u32>,
/// The `minProperties` keyword.
///
/// See [JSON Schema Validation 6.5.2. "minProperties"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.2).
#[serde(skip_serializing_if = "Option::is_none")]
pub min_properties: Option<u32>,
/// The `required` keyword.
///
/// See [JSON Schema Validation 6.5.3. "required"](https://tools.ietf.org/html/draft-handrews-json-schema-validation-02#section-6.5.3).
#[serde(skip_serializing_if = "Set::is_empty")]
pub required: Set<String>,
/// The `properties` keyword.
///
/// See [JSON Schema 9.3.2.1. "properties"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.1).
#[serde(skip_serializing_if = "Map::is_empty")]
pub properties: Map<String, Schema>,
/// The `patternProperties` keyword.
///
/// See [JSON Schema 9.3.2.2. "patternProperties"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.2).
#[serde(skip_serializing_if = "Map::is_empty")]
pub pattern_properties: Map<String, Schema>,
/// The `additionalProperties` keyword.
///
/// See [JSON Schema 9.3.2.3. "additionalProperties"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.3).
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_properties: Option<Box<Schema>>,
/// The `propertyNames` keyword.
///
/// See [JSON Schema 9.3.2.5. "propertyNames"](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-9.3.2.5).
#[serde(skip_serializing_if = "Option::is_none")]
pub property_names: Option<Box<Schema>>,
}
/// The possible types of values in JSON Schema documents.
///
/// See [JSON Schema 4.2.1. Instance Data Model](https://tools.ietf.org/html/draft-handrews-json-schema-02#section-4.2.1).
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))]
#[serde(rename_all = "camelCase")]
pub enum InstanceType {
Null,
Boolean,
Object,
Array,
Number,
String,
Integer,
}
/// A type which can be serialized as a single item, or multiple items.
///
/// In some contexts, a `Single` may be semantically distinct from a `Vec` containing only item.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "impl_json_schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum SingleOrVec<T> {
Single(Box<T>),
Vec(Vec<T>),
}
impl<T> From<T> for SingleOrVec<T> {
fn from(single: T) -> Self {
SingleOrVec::Single(Box::new(single))
}
}
impl<T> From<Vec<T>> for SingleOrVec<T> {
fn from(vec: Vec<T>) -> Self {
SingleOrVec::Vec(vec)
}
}
impl<T: PartialEq> SingleOrVec<T> {
/// Returns `true` if `self` is either a `Single` equal to `x`, or a `Vec` containing `x`.
///
/// # Examples
///
/// ```
/// use schemars::schema::SingleOrVec;
///
/// let s = SingleOrVec::from(10);
/// assert!(s.contains(&10));
/// assert!(!s.contains(&20));
///
/// let v = SingleOrVec::from(vec![10, 20]);
/// assert!(v.contains(&10));
/// assert!(v.contains(&20));
/// assert!(!v.contains(&30));
/// ```
pub fn contains(&self, x: &T) -> bool {
match self {
SingleOrVec::Single(s) => s.deref() == x,
SingleOrVec::Vec(v) => v.contains(x),
map.end()
}
_ => self.0.serialize(serializer),
}
}
}
}

View file

@ -1,8 +1,7 @@
use crate::schema::*;
use crate::JsonSchema;
use crate::{gen::SchemaGenerator, Map};
use serde_json::{Error, Value};
use std::{convert::TryInto, fmt::Display};
use crate::gen::SchemaGenerator;
use crate::{json_schema, JsonSchema, Schema};
use serde_json::{Error, Map, Value};
use std::fmt::Display;
pub(crate) struct Serializer<'a> {
pub(crate) gen: &'a mut SchemaGenerator,
@ -22,7 +21,7 @@ pub(crate) struct SerializeTuple<'a> {
pub(crate) struct SerializeMap<'a> {
gen: &'a mut SchemaGenerator,
properties: Map<String, Schema>,
properties: Map<String, Value>,
current_key: Option<String>,
title: &'static str,
}
@ -36,13 +35,11 @@ macro_rules! forward_to_subschema_for {
}
macro_rules! return_instance_type {
($fn:ident, $ty:ty, $instance_type:ident) => {
($fn:ident, $ty:ty, $instance_type:expr) => {
fn $fn(self, _value: $ty) -> Result<Self::Ok, Self::Error> {
Ok(SchemaObject {
instance_type: Some(InstanceType::$instance_type.into()),
..Default::default()
}
.into())
Ok(json_schema!({
"type": $instance_type
}))
}
};
}
@ -59,18 +56,18 @@ impl<'a> serde::Serializer for Serializer<'a> {
type SerializeStruct = SerializeMap<'a>;
type SerializeStructVariant = Self;
return_instance_type!(serialize_i8, i8, Integer);
return_instance_type!(serialize_i16, i16, Integer);
return_instance_type!(serialize_i32, i32, Integer);
return_instance_type!(serialize_i64, i64, Integer);
return_instance_type!(serialize_i128, i128, Integer);
return_instance_type!(serialize_u8, u8, Integer);
return_instance_type!(serialize_u16, u16, Integer);
return_instance_type!(serialize_u32, u32, Integer);
return_instance_type!(serialize_u64, u64, Integer);
return_instance_type!(serialize_u128, u128, Integer);
return_instance_type!(serialize_f32, f32, Number);
return_instance_type!(serialize_f64, f64, Number);
return_instance_type!(serialize_i8, i8, "integer");
return_instance_type!(serialize_i16, i16, "integer");
return_instance_type!(serialize_i32, i32, "integer");
return_instance_type!(serialize_i64, i64, "integer");
return_instance_type!(serialize_i128, i128, "integer");
return_instance_type!(serialize_u8, u8, "integer");
return_instance_type!(serialize_u16, u16, "integer");
return_instance_type!(serialize_u32, u32, "integer");
return_instance_type!(serialize_u64, u64, "integer");
return_instance_type!(serialize_u128, u128, "integer");
return_instance_type!(serialize_f32, f32, "number");
return_instance_type!(serialize_f64, f64, "number");
forward_to_subschema_for!(serialize_bool, bool);
forward_to_subschema_for!(serialize_char, char);
@ -93,7 +90,7 @@ impl<'a> serde::Serializer for Serializer<'a> {
let value_schema = iter
.into_iter()
.try_fold(None, |acc, (_, v)| {
if acc == Some(Schema::Bool(true)) {
if acc == Some(true.into()) {
return Ok(acc);
}
@ -103,21 +100,16 @@ impl<'a> serde::Serializer for Serializer<'a> {
})?;
Ok(match &acc {
None => Some(schema),
Some(items) if items != &schema => Some(Schema::Bool(true)),
Some(items) if items != &schema => Some(true.into()),
_ => acc,
})
})?
.unwrap_or(Schema::Bool(true));
.unwrap_or(true.into());
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())
Ok(json_schema!({
"type": "object",
"additionalProperties": value_schema,
}))
}
fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
@ -132,52 +124,47 @@ impl<'a> serde::Serializer for Serializer<'a> {
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(self.gen),
Schema::Object(SchemaObject {
instance_type: Some(ref mut instance_type),
..
}) => {
add_null_type(instance_type);
schema
schema = match schema.try_to_object() {
Ok(mut obj) => {
let value = obj.get_mut("type");
match value {
Some(Value::Array(array)) => {
let null = Value::from("null");
if !array.contains(&null) {
array.push(null);
}
obj.into()
}
Some(Value::String(string)) => {
if string != "null" {
*value.unwrap() =
Value::Array(vec![std::mem::take(string).into(), "null".into()])
}
obj.into()
}
_ => json_schema!({
"anyOf": [
obj,
<()>::json_schema(self.gen)
]
}),
}
}
schema => SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
any_of: Some(vec![schema, <()>::json_schema(self.gen)]),
..Default::default()
})),
..Default::default()
}
.into(),
Err(true) => true.into(),
Err(false) => <()>::json_schema(self.gen),
}
}
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);
schema
.ensure_object()
.insert("nullable".into(), true.into());
};
Ok(schema)
@ -193,7 +180,7 @@ impl<'a> serde::Serializer for Serializer<'a> {
_variant_index: u32,
_variant: &'static str,
) -> Result<Self::Ok, Self::Error> {
Ok(Schema::Bool(true))
Ok(true.into())
}
fn serialize_newtype_struct<T: ?Sized>(
@ -205,15 +192,13 @@ impl<'a> serde::Serializer for Serializer<'a> {
T: serde::Serialize,
{
let include_title = self.include_title;
let mut result = value.serialize(self);
let mut schema = value.serialize(self)?;
if include_title {
if let Ok(Schema::Object(ref mut object)) = result {
object.metadata().title = Some(name.to_string());
}
if include_title && !name.is_empty() {
schema.ensure_object().insert("title".into(), name.into());
}
result
Ok(schema)
}
fn serialize_newtype_variant<T: ?Sized>(
@ -226,7 +211,7 @@ impl<'a> serde::Serializer for Serializer<'a> {
where
T: serde::Serialize,
{
Ok(Schema::Bool(true))
Ok(true.into())
}
fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
@ -313,7 +298,7 @@ impl serde::ser::SerializeTupleVariant for Serializer<'_> {
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(Schema::Bool(true))
Ok(true.into())
}
}
@ -333,7 +318,7 @@ impl serde::ser::SerializeStructVariant for Serializer<'_> {
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(Schema::Bool(true))
Ok(true.into())
}
}
@ -345,7 +330,7 @@ impl serde::ser::SerializeSeq for SerializeSeq<'_> {
where
T: serde::Serialize,
{
if self.items != Some(Schema::Bool(true)) {
if self.items != Some(true.into()) {
let schema = value.serialize(Serializer {
gen: self.gen,
include_title: false,
@ -354,7 +339,7 @@ impl serde::ser::SerializeSeq for SerializeSeq<'_> {
None => self.items = Some(schema),
Some(items) => {
if items != &schema {
self.items = Some(Schema::Bool(true))
self.items = Some(true.into())
}
}
}
@ -364,16 +349,12 @@ impl serde::ser::SerializeSeq for SerializeSeq<'_> {
}
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())
let items = self.items.unwrap_or(true.into());
Ok(json_schema!({
"type": "array",
"items": items
}))
}
}
@ -394,23 +375,21 @@ impl serde::ser::SerializeTuple for SerializeTuple<'_> {
}
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()
};
let len = self.items.len();
let mut schema = json_schema!({
"type": "array",
"items": self.items,
"maxItems": len,
"minItems": len,
});
if !self.title.is_empty() {
schema.metadata().title = Some(self.title.to_owned());
schema
.ensure_object()
.insert("title".into(), self.title.into());
}
Ok(schema.into())
Ok(schema)
}
}
@ -459,26 +438,24 @@ impl serde::ser::SerializeMap for SerializeMap<'_> {
gen: self.gen,
include_title: false,
})?;
self.properties.insert(key, schema);
self.properties.insert(key, schema.into());
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()
};
let mut schema = json_schema!({
"type": "object",
"properties": self.properties,
});
if !self.title.is_empty() {
schema.metadata().title = Some(self.title.to_owned());
schema
.ensure_object()
.insert("title".into(), self.title.into());
}
Ok(schema.into())
Ok(schema)
}
}
@ -498,7 +475,7 @@ impl serde::ser::SerializeStruct for SerializeMap<'_> {
gen: self.gen,
include_title: false,
})?;
self.properties.insert(key.to_string(), prop_schema);
self.properties.insert(key.to_string(), prop_schema.into());
Ok(())
}

View file

@ -7,116 +7,87 @@ All methods of `Visitor` have a default implementation that makes no change but
When overriding one of these methods, you will *usually* want to still call this default implementation.
# Example
To add a custom property to all schemas:
To add a custom property to all object schemas:
```
use schemars::schema::SchemaObject;
use schemars::visit::{Visitor, visit_schema_object};
use schemars::Schema;
use schemars::visit::{Visitor, visit_schema};
pub struct MyVisitor;
impl Visitor for MyVisitor {
fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
fn visit_schema(&mut self, schema: &mut Schema) {
// First, make our change to this schema
schema.extensions.insert("my_property".to_string(), serde_json::json!("hello world"));
if let Some(obj) = schema.as_object_mut() {
obj.insert("my_property".to_string(), serde_json::json!("hello world"));
}
// Then delegate to default implementation to visit any subschemas
visit_schema_object(self, schema);
visit_schema(self, schema);
}
}
```
*/
use crate::schema::{RootSchema, Schema, SchemaObject, SingleOrVec};
use serde_json::{json, Value};
use crate::Schema;
/// Trait used to recursively modify a constructed schema and its subschemas.
pub trait Visitor {
/// Override this method to modify a [`RootSchema`] and (optionally) its subschemas.
///
/// When overriding this method, you will usually want to call the [`visit_root_schema`] function to visit subschemas.
fn visit_root_schema(&mut self, root: &mut RootSchema) {
visit_root_schema(self, root)
}
/// Override this method to modify a [`Schema`] and (optionally) its subschemas.
///
/// When overriding this method, you will usually want to call the [`visit_schema`] function to visit subschemas.
fn visit_schema(&mut self, schema: &mut Schema) {
visit_schema(self, schema)
}
/// Override this method to modify a [`SchemaObject`] and (optionally) its subschemas.
///
/// When overriding this method, you will usually want to call the [`visit_schema_object`] function to visit subschemas.
fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
visit_schema_object(self, schema)
}
}
/// Visits all subschemas of the [`RootSchema`].
pub fn visit_root_schema<V: Visitor + ?Sized>(v: &mut V, root: &mut RootSchema) {
v.visit_schema_object(&mut root.schema);
visit_map_values(v, &mut root.definitions);
}
/// Visits all subschemas of the [`Schema`].
pub fn visit_schema<V: Visitor + ?Sized>(v: &mut V, schema: &mut Schema) {
if let Schema::Object(schema) = schema {
v.visit_schema_object(schema)
}
}
/// Visits all subschemas of the [`SchemaObject`].
pub fn visit_schema_object<V: Visitor + ?Sized>(v: &mut V, schema: &mut SchemaObject) {
if let Some(sub) = &mut schema.subschemas {
visit_vec(v, &mut sub.all_of);
visit_vec(v, &mut sub.any_of);
visit_vec(v, &mut sub.one_of);
visit_box(v, &mut sub.not);
visit_box(v, &mut sub.if_schema);
visit_box(v, &mut sub.then_schema);
visit_box(v, &mut sub.else_schema);
}
if let Some(arr) = &mut schema.array {
visit_single_or_vec(v, &mut arr.items);
visit_box(v, &mut arr.additional_items);
visit_box(v, &mut arr.contains);
}
if let Some(obj) = &mut schema.object {
visit_map_values(v, &mut obj.properties);
visit_map_values(v, &mut obj.pattern_properties);
visit_box(v, &mut obj.additional_properties);
visit_box(v, &mut obj.property_names);
}
}
fn visit_box<V: Visitor + ?Sized>(v: &mut V, target: &mut Option<Box<Schema>>) {
if let Some(s) = target {
v.visit_schema(s)
}
}
fn visit_vec<V: Visitor + ?Sized>(v: &mut V, target: &mut Option<Vec<Schema>>) {
if let Some(vec) = target {
for s in vec {
v.visit_schema(s)
}
}
}
fn visit_map_values<V: Visitor + ?Sized>(v: &mut V, target: &mut crate::Map<String, Schema>) {
for s in target.values_mut() {
v.visit_schema(s)
}
}
fn visit_single_or_vec<V: Visitor + ?Sized>(v: &mut V, target: &mut Option<SingleOrVec<Schema>>) {
match target {
None => {}
Some(SingleOrVec::Single(s)) => v.visit_schema(s),
Some(SingleOrVec::Vec(vec)) => {
for s in vec {
v.visit_schema(s)
if let Some(obj) = schema.as_object_mut() {
for (key, value) in obj {
match key.as_str() {
"not"
| "if"
| "then"
| "else"
| "additionalItems"
| "contains"
| "additionalProperties"
| "propertyNames" => {
if let Ok(subschema) = value.try_into() {
v.visit_schema(subschema)
}
}
"allOf" | "anyOf" | "oneOf" => {
if let Some(array) = value.as_array_mut() {
for value in array {
if let Ok(subschema) = value.try_into() {
v.visit_schema(subschema)
}
}
}
}
"items" => {
if let Some(array) = value.as_array_mut() {
for value in array {
if let Ok(subschema) = value.try_into() {
v.visit_schema(subschema)
}
}
} else if let Ok(subschema) = value.try_into() {
v.visit_schema(subschema)
}
}
"properties" | "patternProperties" | "definitions" | "$defs" => {
if let Some(obj) = value.as_object_mut() {
for value in obj.values_mut() {
if let Ok(subschema) = value.try_into() {
v.visit_schema(subschema)
}
}
}
}
_ => {}
}
}
}
@ -133,29 +104,23 @@ pub struct ReplaceBoolSchemas {
impl Visitor for ReplaceBoolSchemas {
fn visit_schema(&mut self, schema: &mut Schema) {
visit_schema(self, schema);
if let Some(obj) = schema.as_object_mut() {
if self.skip_additional_properties {
if let Some((ap_key, ap_value)) = obj.remove_entry("additionalProperties") {
visit_schema(self, schema);
if let Schema::Bool(b) = *schema {
*schema = Schema::Bool(b).into_object().into()
}
}
fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
if self.skip_additional_properties {
if let Some(obj) = &mut schema.object {
if let Some(ap) = &obj.additional_properties {
if let Schema::Bool(_) = ap.as_ref() {
let additional_properties = obj.additional_properties.take();
visit_schema_object(self, schema);
schema.object().additional_properties = additional_properties;
return;
if let Some(obj) = schema.as_object_mut() {
obj.insert(ap_key, ap_value);
}
return;
}
}
}
visit_schema_object(self, schema);
visit_schema(self, schema);
} else {
schema.ensure_object();
}
}
}
@ -166,18 +131,19 @@ impl Visitor for ReplaceBoolSchemas {
pub struct RemoveRefSiblings;
impl Visitor for RemoveRefSiblings {
fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
visit_schema_object(self, schema);
fn visit_schema(&mut self, schema: &mut Schema) {
visit_schema(self, schema);
if let Some(reference) = schema.reference.take() {
if schema == &SchemaObject::default() {
schema.reference = Some(reference);
} else {
let ref_schema = Schema::new_ref(reference);
let all_of = &mut schema.subschemas().all_of;
match all_of {
Some(vec) => vec.push(ref_schema),
None => *all_of = Some(vec![ref_schema]),
if let Some(obj) = schema.as_object_mut() {
if obj.len() > 1 {
if let Some(ref_value) = obj.remove("$ref") {
if let Value::Array(all_of) =
obj.entry("allOf").or_insert(Value::Array(Vec::new()))
{
all_of.push(json!({
"$ref": ref_value
}));
}
}
}
}
@ -188,25 +154,18 @@ impl Visitor for RemoveRefSiblings {
///
/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support the `examples` property.
#[derive(Debug, Clone)]
pub struct SetSingleExample {
/// When set to `true`, the `examples` property will not be removed, but its first value will still be copied to `example`.
pub retain_examples: bool,
}
pub struct SetSingleExample;
impl Visitor for SetSingleExample {
fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
visit_schema_object(self, schema);
fn visit_schema(&mut self, schema: &mut Schema) {
visit_schema(self, schema);
let first_example = schema.metadata.as_mut().and_then(|m| {
if self.retain_examples {
m.examples.first().cloned()
} else {
m.examples.drain(..).next()
if let Some(obj) = schema.as_object_mut() {
if let Some(Value::Array(examples)) = obj.remove("examples") {
if let Some(first_example) = examples.into_iter().next() {
obj.insert("example".into(), first_example);
}
}
});
if let Some(example) = first_example {
schema.extensions.insert("example".to_owned(), example);
}
}
}

View file

@ -1,16 +1,6 @@
mod util;
use util::*;
#[test]
fn arrayvec05() -> TestResult {
test_default_generated_schema::<arrayvec05::ArrayVec<[i32; 16]>>("arrayvec")
}
#[test]
fn arrayvec05_string() -> TestResult {
test_default_generated_schema::<arrayvec05::ArrayString<[u8; 16]>>("arrayvec_string")
}
#[test]
fn arrayvec07() -> TestResult {
test_default_generated_schema::<arrayvec07::ArrayVec<i32, 16>>("arrayvec")

View file

@ -1,5 +1,5 @@
mod util;
use bytes::{Bytes, BytesMut};
use bytes1::{Bytes, BytesMut};
use util::*;
#[test]

View file

@ -1,5 +1,5 @@
mod util;
use chrono::prelude::*;
use chrono04::prelude::*;
use schemars::JsonSchema;
use util::*;

View file

@ -3,12 +3,7 @@ use util::*;
#[test]
fn rust_decimal() -> TestResult {
test_default_generated_schema::<rust_decimal::Decimal>("rust_decimal")
}
#[test]
fn bigdecimal03() -> TestResult {
test_default_generated_schema::<bigdecimal03::BigDecimal>("bigdecimal03")
test_default_generated_schema::<rust_decimal1::Decimal>("rust_decimal")
}
#[test]

View file

@ -1,23 +0,0 @@
use schemars::{gen::SchemaGenerator, JsonSchema};
use std::ptr;
#[allow(dead_code)]
#[derive(JsonSchema)]
struct Struct {
foo: i32,
bar: bool,
}
#[test]
fn dereference_struct() {
let mut gen = SchemaGenerator::default();
let struct_ref_schema = gen.subschema_for::<Struct>();
let struct_schema = gen.definitions().get(&<Struct>::schema_name()).unwrap();
assert!(struct_ref_schema.is_ref());
assert!(!struct_schema.is_ref());
let dereferenced = gen.dereference(&struct_ref_schema);
assert!(dereferenced.is_some());
assert!(ptr::eq(dereferenced.unwrap(), struct_schema));
}

View file

@ -1,5 +1,5 @@
mod util;
use either::Either;
use either1::Either;
use util::*;
#[test]

View file

@ -1,5 +1,7 @@
mod util;
use schemars::{JsonSchema, Map};
use std::collections::BTreeMap;
use schemars::JsonSchema;
use util::*;
// Ensure that schemars_derive uses the full path to std::string::String
@ -20,7 +22,7 @@ struct Struct {
#[schemars(rename_all = "camelCase")]
enum External {
UnitOne,
StringMap(Map<&'static str, &'static str>),
StringMap(BTreeMap<&'static str, &'static str>),
UnitStructNewType(UnitStruct),
StructNewType(Struct),
Struct {
@ -29,6 +31,7 @@ enum External {
},
UnitTwo,
Tuple(i32, bool),
// FIXME this should probably only replace the "payload" of the enum
#[schemars(with = "i32")]
WithInt,
}
@ -43,7 +46,7 @@ fn enum_external_tag() -> TestResult {
#[schemars(tag = "typeProperty")]
enum Internal {
UnitOne,
StringMap(Map<&'static str, &'static str>),
StringMap(BTreeMap<&'static str, &'static str>),
UnitStructNewType(UnitStruct),
StructNewType(Struct),
Struct {
@ -51,6 +54,7 @@ enum Internal {
bar: bool,
},
UnitTwo,
// FIXME this should probably only replace the "payload" of the enum
#[schemars(with = "i32")]
WithInt,
}
@ -65,7 +69,7 @@ fn enum_internal_tag() -> TestResult {
#[schemars(untagged)]
enum Untagged {
UnitOne,
StringMap(Map<&'static str, &'static str>),
StringMap(BTreeMap<&'static str, &'static str>),
UnitStructNewType(UnitStruct),
StructNewType(Struct),
Struct {
@ -73,6 +77,7 @@ enum Untagged {
bar: bool,
},
Tuple(i32, bool),
// FIXME this should probably only replace the "payload" of the enum
#[schemars(with = "i32")]
WithInt,
}
@ -87,7 +92,7 @@ fn enum_untagged() -> TestResult {
#[schemars(tag = "t", content = "c")]
enum Adjacent {
UnitOne,
StringMap(Map<&'static str, &'static str>),
StringMap(BTreeMap<&'static str, &'static str>),
UnitStructNewType(UnitStruct),
StructNewType(Struct),
Struct {
@ -96,6 +101,7 @@ enum Adjacent {
},
Tuple(i32, bool),
UnitTwo,
// FIXME this should probably only replace the "payload" of the enum
#[schemars(with = "i32")]
WithInt,
}

View file

@ -1,5 +1,7 @@
mod util;
use schemars::{JsonSchema, Map};
use std::collections::BTreeMap;
use schemars::JsonSchema;
use util::*;
// Ensure that schemars_derive uses the full path to std::string::String
@ -22,7 +24,7 @@ struct Struct {
#[schemars(rename_all = "camelCase", deny_unknown_fields)]
enum External {
UnitOne,
StringMap(Map<&'static str, &'static str>),
StringMap(BTreeMap<&'static str, &'static str>),
UnitStructNewType(UnitStruct),
StructNewType(Struct),
Struct {
@ -31,6 +33,7 @@ enum External {
},
UnitTwo,
Tuple(i32, bool),
// FIXME this should probably only replace the "payload" of the enum
#[schemars(with = "i32")]
WithInt,
}
@ -46,7 +49,7 @@ fn enum_external_tag() -> TestResult {
#[schemars(tag = "typeProperty", deny_unknown_fields)]
enum Internal {
UnitOne,
StringMap(Map<&'static str, &'static str>),
StringMap(BTreeMap<&'static str, &'static str>),
UnitStructNewType(UnitStruct),
StructNewType(Struct),
Struct {
@ -54,6 +57,7 @@ enum Internal {
bar: bool,
},
UnitTwo,
// FIXME this should only replace the "payload" of the enum (which doesn't even make sense for unit enums!)
#[schemars(with = "i32")]
WithInt,
}
@ -69,7 +73,7 @@ fn enum_internal_tag() -> TestResult {
#[schemars(untagged, deny_unknown_fields)]
enum Untagged {
UnitOne,
StringMap(Map<&'static str, &'static str>),
StringMap(BTreeMap<&'static str, &'static str>),
UnitStructNewType(UnitStruct),
StructNewType(Struct),
Struct {
@ -77,6 +81,7 @@ enum Untagged {
bar: bool,
},
Tuple(i32, bool),
// FIXME this should probably only replace the "payload" of the enum
#[schemars(with = "i32")]
WithInt,
}
@ -92,7 +97,7 @@ fn enum_untagged() -> TestResult {
#[schemars(tag = "t", content = "c", deny_unknown_fields)]
enum Adjacent {
UnitOne,
StringMap(Map<&'static str, &'static str>),
StringMap(BTreeMap<&'static str, &'static str>),
UnitStructNewType(UnitStruct),
StructNewType(Struct),
Struct {
@ -101,6 +106,7 @@ enum Adjacent {
},
Tuple(i32, bool),
UnitTwo,
// FIXME this should probably only replace the "payload" of the enum
#[schemars(with = "i32")]
WithInt,
}

View file

@ -1,8 +1,11 @@
mod util;
use enumset::{EnumSet, EnumSetType};
use enumset1::{EnumSet, EnumSetType};
use schemars::JsonSchema;
use util::*;
// needed to derive EnumSetType when using a crate alias
extern crate enumset1 as enumset;
#[derive(EnumSetType, JsonSchema)]
enum Foo {
Bar,

View file

@ -1,5 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "String",
"title": "string",
"type": "string"
}

View file

@ -1,6 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Decimal",
"type": "string",
"pattern": "^-?[0-9]+(\\.[0-9]+)?$"
}

View file

@ -1,6 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Either_int32_or_Either_Boolean_or_Null",
"title": "Either_int32_or_Either_boolean_or_null",
"anyOf": [
{
"type": "integer",

View file

@ -127,8 +127,7 @@
"WithInt"
]
}
},
"additionalProperties": false
}
}
]
}

View file

@ -15,7 +15,7 @@
"$ref": "#/definitions/Range_of_double"
},
"bound": {
"$ref": "#/definitions/Bound_of_String"
"$ref": "#/definitions/Bound_of_string"
}
},
"definitions": {
@ -55,7 +55,7 @@
}
}
},
"Bound_of_String": {
"Bound_of_string": {
"oneOf": [
{
"type": "object",

View file

@ -10,10 +10,10 @@
],
"properties": {
"byte_or_bool2": {
"$ref": "#/definitions/Or_for_uint8_and_Boolean"
"$ref": "#/definitions/Or_for_uint8_and_boolean"
},
"unit_or_t2": {
"$ref": "#/definitions/Or_for_Null_and_int32"
"$ref": "#/definitions/Or_for_null_and_int32"
},
"s": {
"$ref": "#/definitions/Str"
@ -30,7 +30,7 @@
}
},
"definitions": {
"Or_for_uint8_and_Boolean": {
"Or_for_uint8_and_boolean": {
"anyOf": [
{
"type": "integer",
@ -42,7 +42,7 @@
}
]
},
"Or_for_Null_and_int32": {
"Or_for_null_and_int32": {
"anyOf": [
{
"type": "null"

View file

@ -8,14 +8,14 @@
],
"properties": {
"result1": {
"$ref": "#/definitions/Result_of_MyStruct_or_Array_of_String"
"$ref": "#/definitions/Result_of_MyStruct_or_Array_of_string"
},
"result2": {
"$ref": "#/definitions/Result_of_Boolean_or_Null"
"$ref": "#/definitions/Result_of_boolean_or_null"
}
},
"definitions": {
"Result_of_MyStruct_or_Array_of_String": {
"Result_of_MyStruct_or_Array_of_string": {
"oneOf": [
{
"type": "object",
@ -56,7 +56,7 @@
}
}
},
"Result_of_Boolean_or_Null": {
"Result_of_boolean_or_null": {
"oneOf": [
{
"type": "object",

View file

@ -1,6 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "a-new-name-Array_of_String-int32-int32",
"title": "a-new-name-Array_of_string-int32-int32",
"type": "object",
"required": [
"inner",

View file

@ -1,15 +1,11 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MyStruct_for_int32_and_Null_and_Boolean_and_Array_of_String",
"title": "MyStruct_for_int32_and_null_and_boolean_and_Array_of_string",
"type": "object",
"required": [
"inner",
"t",
"u",
"v",
"w"
],
"properties": {
"inner": {
"$ref": "#/definitions/MySimpleStruct"
},
"t": {
"type": "integer",
"format": "int32"
@ -25,23 +21,27 @@
"items": {
"type": "string"
}
},
"inner": {
"$ref": "#/definitions/MySimpleStruct"
}
},
"required": [
"inner",
"t",
"u",
"v",
"w"
],
"definitions": {
"MySimpleStruct": {
"type": "object",
"required": [
"foo"
],
"properties": {
"foo": {
"type": "integer",
"format": "int32"
}
}
},
"required": [
"foo"
]
}
}
}

View file

@ -1,6 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MixedGenericStruct_for_MyStruct_for_int32_and_Null_and_Boolean_and_Array_of_String_and_42_and_z",
"title": "MixedGenericStruct_for_MyStruct_for_int32_and_null_and_boolean_and_Array_of_string_and_42_and_z",
"type": "object",
"required": [
"foo",
@ -12,7 +12,7 @@
"format": "int32"
},
"generic": {
"$ref": "#/definitions/MyStruct_for_int32_and_Null_and_Boolean_and_Array_of_String"
"$ref": "#/definitions/MyStruct_for_int32_and_null_and_boolean_and_Array_of_string"
}
},
"definitions": {
@ -28,7 +28,7 @@
}
}
},
"MyStruct_for_int32_and_Null_and_Boolean_and_Array_of_String": {
"MyStruct_for_int32_and_null_and_boolean_and_Array_of_string": {
"type": "object",
"required": [
"inner",

View file

@ -22,8 +22,8 @@
},
{
"type": [
"boolean",
"object"
"object",
"boolean"
],
"required": [
"typeProperty"
@ -39,8 +39,8 @@
},
{
"type": [
"boolean",
"object"
"object",
"boolean"
],
"required": [
"typeProperty"

View file

@ -1,6 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Array_of_String",
"title": "Array_of_string",
"type": "array",
"items": {
"type": "string"

View file

@ -1,5 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "String",
"title": "string",
"type": "string"
}

View file

@ -1,13 +1,15 @@
mod util;
use indexmap::{IndexMap, IndexSet};
use std::hash::RandomState;
use indexmap2::{IndexMap, IndexSet};
use schemars::JsonSchema;
use util::*;
#[allow(dead_code)]
#[derive(JsonSchema)]
struct IndexMapTypes {
map: IndexMap<i32, bool>,
set: IndexSet<isize>,
map: IndexMap<i32, bool, RandomState>,
set: IndexSet<isize, RandomState>,
}
#[test]

View file

@ -1,16 +0,0 @@
mod util;
use indexmap2::{IndexMap, IndexSet};
use schemars::JsonSchema;
use util::*;
#[allow(dead_code)]
#[derive(JsonSchema)]
struct IndexMapTypes {
map: IndexMap<i32, bool>,
set: IndexSet<isize>,
}
#[test]
fn indexmap_types() -> TestResult {
test_default_generated_schema::<IndexMapTypes>("indexmap")
}

View file

@ -1,19 +0,0 @@
mod util;
use schemars::gen::SchemaSettings;
use schemars::schema::RootSchema;
use util::*;
#[test]
fn schema_matches_draft07() -> TestResult {
test_generated_schema::<RootSchema>("schema", SchemaSettings::draft07())
}
#[test]
fn schema_matches_2019_09() -> TestResult {
test_generated_schema::<RootSchema>("schema-2019_09", SchemaSettings::draft2019_09())
}
#[test]
fn schema_matches_openapi3() -> TestResult {
test_generated_schema::<RootSchema>("schema-openapi3", SchemaSettings::openapi3())
}

View file

@ -2,7 +2,7 @@ mod util;
use schemars::JsonSchema;
use util::*;
fn schema_fn(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
fn schema_fn(gen: &mut schemars::gen::SchemaGenerator) -> schemars::Schema {
<bool>::json_schema(gen)
}
@ -21,6 +21,7 @@ pub enum External {
#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema,
i32,
),
// FIXME this should probably only replace the "payload" of the enum
#[schemars(schema_with = "schema_fn")]
Unit,
}
@ -38,6 +39,7 @@ pub enum Internal {
foo: DoesntImplementJsonSchema,
},
NewType(#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema),
// FIXME this should probably only replace the "payload" of the enum
#[schemars(schema_with = "schema_fn")]
Unit,
}
@ -59,6 +61,7 @@ pub enum Untagged {
#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema,
i32,
),
// FIXME this should probably only replace the "payload" of the enum
#[schemars(schema_with = "schema_fn")]
Unit,
}
@ -80,6 +83,7 @@ pub enum Adjacent {
#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema,
i32,
),
// FIXME this should probably only replace the "payload" of the enum
#[schemars(schema_with = "schema_fn")]
Unit,
}

View file

@ -2,7 +2,7 @@ mod util;
use schemars::JsonSchema;
use util::*;
fn schema_fn(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
fn schema_fn(gen: &mut schemars::gen::SchemaGenerator) -> schemars::Schema {
<bool>::json_schema(gen)
}

View file

@ -1,6 +1,6 @@
mod util;
use schemars::JsonSchema;
use semver::Version;
use semver1::Version;
use util::*;
#[allow(dead_code)]

View file

@ -1,5 +1,5 @@
mod util;
use smallvec::SmallVec;
use smallvec1::SmallVec;
use util::*;
#[test]

View file

@ -1,5 +1,5 @@
mod util;
use smol_str::SmolStr;
use smol_str02::SmolStr;
use util::*;
#[test]

View file

@ -1,6 +1,6 @@
mod util;
use schemars::JsonSchema;
use url::Url;
use url2::Url;
use util::*;
#[allow(dead_code)]

View file

@ -1,5 +1,6 @@
use pretty_assertions::assert_eq;
use schemars::{gen::SchemaSettings, schema::RootSchema, schema_for, JsonSchema};
use schemars::visit::Visitor;
use schemars::{gen::SchemaSettings, schema_for, JsonSchema, Schema};
use std::error::Error;
use std::fs;
@ -17,7 +18,16 @@ pub fn test_default_generated_schema<T: JsonSchema>(file: &str) -> TestResult {
test_schema(&actual, file)
}
pub fn test_schema(actual: &RootSchema, file: &str) -> TestResult {
pub fn test_schema(actual: &Schema, file: &str) -> TestResult {
// TEMP for easier comparison of schemas handling changes that don't actually affect a schema:
// - `required` ordering has changed
// - previously `f64` properties may now be integers
let actual = &{
let mut actual = actual.clone();
TempFixupForTests.visit_schema(&mut actual);
actual
};
let expected_json = match fs::read_to_string(format!("tests/expected/{}.json", file)) {
Ok(j) => j,
Err(e) => {
@ -35,8 +45,30 @@ pub fn test_schema(actual: &RootSchema, file: &str) -> TestResult {
Ok(())
}
fn write_actual_to_file(schema: &RootSchema, file: &str) -> TestResult {
fn write_actual_to_file(schema: &Schema, file: &str) -> TestResult {
let actual_json = serde_json::to_string_pretty(&schema)?;
fs::write(format!("tests/actual/{}.json", file), actual_json)?;
Ok(())
}
struct TempFixupForTests;
impl schemars::visit::Visitor for TempFixupForTests {
fn visit_schema(&mut self, schema: &mut Schema) {
schemars::visit::visit_schema(self, schema);
if let Some(object) = schema.as_object_mut() {
if let Some(serde_json::Value::Array(required)) = object.get_mut("required") {
required.sort_unstable_by(|a, b| a.as_str().cmp(&b.as_str()));
}
for (key, value) in object {
if key == "multipleOf" || key.ends_with("aximum") || key.ends_with("inimum") {
if let Some(f) = value.as_f64() {
*value = f.into();
}
}
}
}
}
}

View file

@ -1,11 +1,6 @@
mod util;
use util::*;
#[test]
fn uuid08() -> TestResult {
test_default_generated_schema::<uuid08::Uuid>("uuid")
}
#[test]
fn uuid1() -> TestResult {
test_default_generated_schema::<uuid1::Uuid>("uuid")

View file

@ -325,119 +325,85 @@ impl ValidationAttrs {
}
pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) {
if let Some(apply_expr) = self.apply_to_schema_expr() {
*schema_expr = quote! {
{
let mut schema = #schema_expr;
#apply_expr
schema
}
}
let setters = self.make_setters(quote!(&mut schema));
if !setters.is_empty() {
*schema_expr = quote!({
let mut schema = #schema_expr;
#(#setters)*
schema
});
}
}
fn apply_to_schema_expr(&self) -> Option<TokenStream> {
let mut array_validation = Vec::new();
let mut number_validation = Vec::new();
let mut object_validation = Vec::new();
let mut string_validation = Vec::new();
fn make_setters(&self, mut_schema: impl ToTokens) -> Vec<TokenStream> {
let mut result = Vec::new();
if let Some(length_min) = self.length_min.as_ref().or(self.length_equal.as_ref()) {
string_validation.push(quote! {
validation.min_length = Some(#length_min as u32);
result.push(quote! {
schemars::_private::insert_validation_property(#mut_schema, "string", "minLength", #length_min);
});
array_validation.push(quote! {
validation.min_items = Some(#length_min as u32);
result.push(quote! {
schemars::_private::insert_validation_property(#mut_schema, "array", "minItems", #length_min);
});
}
if let Some(length_max) = self.length_max.as_ref().or(self.length_equal.as_ref()) {
string_validation.push(quote! {
validation.max_length = Some(#length_max as u32);
result.push(quote! {
schemars::_private::insert_validation_property(#mut_schema, "string", "maxLength", #length_max);
});
array_validation.push(quote! {
validation.max_items = Some(#length_max as u32);
result.push(quote! {
schemars::_private::insert_validation_property(#mut_schema, "array", "maxItems", #length_max);
});
}
if let Some(range_min) = &self.range_min {
number_validation.push(quote! {
validation.minimum = Some(#range_min as f64);
result.push(quote! {
schemars::_private::insert_validation_property(#mut_schema, "number", "minimum", #range_min);
});
}
if let Some(range_max) = &self.range_max {
number_validation.push(quote! {
validation.maximum = Some(#range_max as f64);
result.push(quote! {
schemars::_private::insert_validation_property(#mut_schema, "number", "maximum", #range_max);
});
}
if let Some(regex) = &self.regex {
string_validation.push(quote! {
validation.pattern = Some(#regex.to_string());
result.push(quote! {
schemars::_private::insert_validation_property(#mut_schema, "string", "pattern", #regex);
});
}
if let Some(contains) = &self.contains {
object_validation.push(quote! {
validation.required.insert(#contains.to_string());
result.push(quote! {
schemars::_private::append_required(#mut_schema, #contains);
});
if self.regex.is_none() {
let pattern = crate::regex_syntax::escape(contains);
string_validation.push(quote! {
validation.pattern = Some(#pattern.to_string());
result.push(quote! {
schemars::_private::insert_validation_property(#mut_schema, "string", "pattern", #pattern);
});
}
}
let format = self.format.as_ref().map(|f| {
let f = f.schema_str();
quote! {
schema_object.format = Some(#f.to_string());
}
});
let inner_validation = self
.inner
.as_deref()
.and_then(|inner| inner.apply_to_schema_expr())
.map(|apply_expr| {
quote! {
if schema_object.has_type(schemars::schema::InstanceType::Array) {
if let Some(schemars::schema::SingleOrVec::Single(inner_schema)) = &mut schema_object.array().items {
let mut schema = &mut **inner_schema;
#apply_expr
}
}
}
});
let array_validation = wrap_array_validation(array_validation);
let number_validation = wrap_number_validation(number_validation);
let object_validation = wrap_object_validation(object_validation);
let string_validation = wrap_string_validation(string_validation);
if array_validation.is_some()
|| number_validation.is_some()
|| object_validation.is_some()
|| string_validation.is_some()
|| format.is_some()
|| inner_validation.is_some()
{
Some(quote! {
if let schemars::schema::Schema::Object(schema_object) = &mut schema {
#array_validation
#number_validation
#object_validation
#string_validation
#format
#inner_validation
}
if let Some(format) = &self.format {
let f = format.schema_str();
result.push(quote! {
schema.ensure_object().insert("format".to_owned(), #f.into());
})
} else {
None
};
if let Some(inner) = &self.inner {
let inner_setters = inner.make_setters(quote!(schema));
if !inner_setters.is_empty() {
result.push(quote! {
schemars::_private::apply_inner_validation(#mut_schema, |schema| { #(#inner_setters)* });
})
}
}
result
}
}
@ -456,59 +422,6 @@ fn parse_lit_into_expr_path(
})
}
fn wrap_array_validation(v: Vec<TokenStream>) -> Option<TokenStream> {
if v.is_empty() {
None
} else {
Some(quote! {
if schema_object.has_type(schemars::schema::InstanceType::Array) {
let validation = schema_object.array();
#(#v)*
}
})
}
}
fn wrap_number_validation(v: Vec<TokenStream>) -> Option<TokenStream> {
if v.is_empty() {
None
} else {
Some(quote! {
if schema_object.has_type(schemars::schema::InstanceType::Integer)
|| schema_object.has_type(schemars::schema::InstanceType::Number) {
let validation = schema_object.number();
#(#v)*
}
})
}
}
fn wrap_object_validation(v: Vec<TokenStream>) -> Option<TokenStream> {
if v.is_empty() {
None
} else {
Some(quote! {
if schema_object.has_type(schemars::schema::InstanceType::Object) {
let validation = schema_object.object();
#(#v)*
}
})
}
}
fn wrap_string_validation(v: Vec<TokenStream>) -> Option<TokenStream> {
if v.is_empty() {
None
} else {
Some(quote! {
if schema_object.has_type(schemars::schema::InstanceType::String) {
let validation = schema_object.string();
#(#v)*
}
})
}
}
fn str_or_num_to_expr(cx: &Ctxt, meta_item_name: &str, expr: Expr) -> Option<Expr> {
// this odd double-parsing is to make `-10` parsed as an Lit instead of an Expr::Unary
let lit: Lit = match syn::parse2(expr.to_token_stream()) {

View file

@ -68,11 +68,11 @@ fn derive_json_schema(mut input: syn::DeriveInput, repr: bool) -> syn::Result<To
<#ty as schemars::JsonSchema>::schema_id()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::Schema {
<#ty as schemars::JsonSchema>::json_schema(gen)
}
fn _schemars_private_non_optional_json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
fn _schemars_private_non_optional_json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::Schema {
<#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(gen)
}
@ -182,7 +182,7 @@ fn derive_json_schema(mut input: syn::DeriveInput, repr: bool) -> syn::Result<To
#schema_id
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::Schema {
#schema_expr
}
};

View file

@ -13,32 +13,46 @@ pub struct SchemaMetadata<'a> {
impl<'a> SchemaMetadata<'a> {
pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) {
let setters = self.make_setters();
if !setters.is_empty() {
*schema_expr = quote! {{
let mut schema = #schema_expr;
let obj = schema.ensure_object();
#(#setters)*
schema
}}
}
}
fn make_setters(&self) -> Vec<TokenStream> {
let mut setters = Vec::<TokenStream>::new();
if let Some(title) = &self.title {
*schema_expr = quote! {
schemars::_private::metadata::add_title(#schema_expr, #title)
};
setters.push(quote! {
obj.insert("title".to_owned(), #title.into());
});
}
if let Some(description) = &self.description {
*schema_expr = quote! {
schemars::_private::metadata::add_description(#schema_expr, #description)
};
setters.push(quote! {
obj.insert("description".to_owned(), #description.into());
});
}
if self.deprecated {
*schema_expr = quote! {
schemars::_private::metadata::add_deprecated(#schema_expr, true)
};
setters.push(quote! {
obj.insert("deprecated".to_owned(), true.into());
});
}
if self.read_only {
*schema_expr = quote! {
schemars::_private::metadata::add_read_only(#schema_expr, true)
};
setters.push(quote! {
obj.insert("readOnly".to_owned(), true.into());
});
}
if self.write_only {
*schema_expr = quote! {
schemars::_private::metadata::add_write_only(#schema_expr, true)
};
setters.push(quote! {
obj.insert("writeOnly".to_owned(), true.into());
});
}
if !self.examples.is_empty() {
@ -47,16 +61,19 @@ impl<'a> SchemaMetadata<'a> {
schemars::_serde_json::value::to_value(#eg())
}
});
*schema_expr = quote! {
schemars::_private::metadata::add_examples(#schema_expr, [#(#examples),*].into_iter().flatten())
};
setters.push(quote! {
obj.insert("examples".to_owned(), schemars::_serde_json::Value::Array([#(#examples),*].into_iter().flatten().collect()));
});
}
if let Some(default) = &self.default {
*schema_expr = quote! {
schemars::_private::metadata::add_default(#schema_expr, #default.and_then(|d| schemars::_schemars_maybe_to_value!(d)))
};
setters.push(quote! {
if let Some(default) = #default.and_then(|d| schemars::_schemars_maybe_to_value!(d)) {
obj.insert("default".to_owned(), default);
}
});
}
setters
}
}

View file

@ -49,9 +49,18 @@ pub fn expr_for_repr(cont: &Container) -> Result<TokenStream, syn::Error> {
let enum_ident = &cont.ident;
let variant_idents = variants.iter().map(|v| &v.ident);
let mut schema_expr = schema_object(quote! {
instance_type: Some(schemars::schema::InstanceType::Integer.into()),
enum_values: Some(vec![#((#enum_ident::#variant_idents as #repr_type).into()),*]),
let mut schema_expr = quote!({
let mut map = schemars::_serde_json::Map::new();
map.insert("type".to_owned(), "integer".into());
map.insert(
"enum".to_owned(),
schemars::_serde_json::Value::Array({
let mut enum_values = Vec::new();
#(enum_values.push((#enum_ident::#variant_idents as #repr_type).into());)*
enum_values
}),
);
schemars::Schema::from(map)
});
cont.attrs.as_metadata().apply_to_schema(&mut schema_expr);
@ -118,7 +127,7 @@ fn type_for_schema(with_attr: &WithAttr) -> (syn::Type, Option<TokenStream>) {
))
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::Schema {
#fun(gen)
}
}
@ -160,9 +169,18 @@ fn expr_for_external_tagged_enum<'a>(
})
.partition(|v| v.is_unit() && v.attrs.is_default());
let unit_names = unit_variants.iter().map(|v| v.name());
let unit_schema = schema_object(quote! {
instance_type: Some(schemars::schema::InstanceType::String.into()),
enum_values: Some(vec![#(#unit_names.into()),*]),
let unit_schema = quote!({
let mut map = schemars::_serde_json::Map::new();
map.insert("type".to_owned(), "string".into());
map.insert(
"enum".to_owned(),
schemars::_serde_json::Value::Array({
let mut enum_values = Vec::new();
#(enum_values.push((#unit_names).into());)*
enum_values
}),
);
schemars::Schema::from(map)
});
if complex_variants.is_empty() {
@ -276,47 +294,44 @@ fn expr_for_adjacent_tagged_enum<'a>(
let (add_content_to_props, add_content_to_required) = content_schema
.map(|content_schema| {
(
quote!(props.insert(#content_name.to_owned(), #content_schema);),
quote!(required.insert(#content_name.to_owned());),
quote!(#content_name: (#content_schema),),
quote!(#content_name,),
)
})
.unwrap_or_default();
let name = variant.name();
let tag_schema = schema_object(quote! {
instance_type: Some(schemars::schema::InstanceType::String.into()),
enum_values: Some(vec![#name.into()]),
});
let tag_schema = quote! {
schemars::json_schema!({
"type": "string",
"enum": [#name],
})
};
let set_additional_properties = if deny_unknown_fields {
quote! {
additional_properties: Some(Box::new(false.into())),
"additionalProperties": false,
}
} else {
TokenStream::new()
};
let mut outer_schema = schema_object(quote! {
instance_type: Some(schemars::schema::InstanceType::Object.into()),
object: Some(Box::new(schemars::schema::ObjectValidation {
properties: {
let mut props = schemars::Map::new();
props.insert(#tag_name.to_owned(), #tag_schema);
let mut outer_schema = quote! {
schemars::json_schema!({
"type": "object",
"properties": {
#tag_name: (#tag_schema),
#add_content_to_props
props
},
required: {
let mut required = schemars::Set::new();
required.insert(#tag_name.to_owned());
"required": [
#tag_name,
#add_content_to_required
required
},
],
// As we're creating a "wrapper" object, we can honor the
// disposition of deny_unknown_fields.
#set_additional_properties
..Default::default()
})),
});
})
};
variant
.attrs
@ -333,21 +348,19 @@ fn expr_for_adjacent_tagged_enum<'a>(
/// Callers must determine if all subschemas are mutually exclusive. This can
/// be done for most tagging regimes by checking that all tag names are unique.
fn variant_subschemas(unique: bool, schemas: Vec<TokenStream>) -> TokenStream {
if unique {
schema_object(quote! {
subschemas: Some(Box::new(schemars::schema::SubschemaValidation {
one_of: Some(vec![#(#schemas),*]),
..Default::default()
})),
})
} else {
schema_object(quote! {
subschemas: Some(Box::new(schemars::schema::SubschemaValidation {
any_of: Some(vec![#(#schemas),*]),
..Default::default()
})),
})
}
let keyword = if unique { "oneOf" } else { "anyOf" };
quote!({
let mut map = schemars::_serde_json::Map::new();
map.insert(
#keyword.to_owned(),
schemars::_serde_json::Value::Array({
let mut enum_values = Vec::new();
#(enum_values.push(#schemas.to_value());)*
enum_values
}),
);
schemars::Schema::from(map)
})
}
fn expr_for_untagged_enum_variant(variant: &Variant, deny_unknown_fields: bool) -> TokenStream {
@ -412,16 +425,11 @@ fn expr_for_tuple_struct(fields: &[Field]) -> TokenStream {
let len = fields.len() as u32;
quote! {
schemars::schema::Schema::Object(
schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::Array.into()),
array: Some(Box::new(schemars::schema::ArrayValidation {
items: Some(vec![#(#fields),*].into()),
max_items: Some(#len),
min_items: Some(#len),
..Default::default()
})),
..Default::default()
schemars::json_schema!({
"type": "array",
"items": [#((#fields)),*],
"minItems": #len,
"maxItems": #len,
})
}
}
@ -477,7 +485,7 @@ fn expr_for_struct(
quote! {
{
#type_def
schemars::_private::insert_object_property::<#ty>(object_validation, #name, #has_default, #required, #schema_expr);
schemars::_private::insert_object_property::<#ty>(&mut schema, #name, #has_default, #required, #schema_expr);
}
}
})
@ -502,7 +510,7 @@ fn expr_for_struct(
let set_additional_properties = if deny_unknown_fields {
quote! {
object_validation.additional_properties = Some(Box::new(false.into()));
"additionalProperties": false,
}
} else {
TokenStream::new()
@ -510,15 +518,12 @@ fn expr_for_struct(
quote! {
{
#set_container_default
let mut schema_object = schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::Object.into()),
..Default::default()
};
let object_validation = schema_object.object();
#set_additional_properties
let mut schema = schemars::json_schema!({
"type": "object",
#set_additional_properties
});
#(#properties)*
schemars::schema::Schema::Object(schema_object)
#(.flatten(#flattens))*
schema #(.flatten(#flattens))*
}
}
}
@ -578,16 +583,6 @@ fn field_default_expr(field: &Field, container_has_default: bool) -> Option<Toke
})
}
fn schema_object(properties: TokenStream) -> TokenStream {
quote! {
schemars::schema::Schema::Object(
schemars::schema::SchemaObject {
#properties
..Default::default()
})
}
}
fn prepend_type_def(type_def: Option<TokenStream>, schema_expr: &mut TokenStream) {
if let Some(type_def) = type_def {
*schema_expr = quote! {