use crate::common::bool_or::BoolOr;
use crate::common::formats::{IntegerFormat, NumberFormat, SchemaType, StringFormat};
use crate::common::helpers::validate_pattern;
use crate::common::reference::RefOr;
use crate::v3_2::discriminator::Discriminator;
use crate::v3_2::external_documentation::ExternalDocumentation;
use crate::v3_2::spec::Spec;
use crate::v3_2::xml::XML;
use crate::validation::{Context, PushError, ValidateWithContext};
use monostate::MustBe;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashSet};
use std::fmt::{Display, Formatter};
#[derive(Clone, Debug, Serialize, PartialEq)]
#[serde(untagged)]
pub enum Schema {
Bool(bool),
AllOf(Box<AllOfSchema>),
AnyOf(Box<AnyOfSchema>),
OneOf(Box<OneOfSchema>),
Not(Box<NotSchema>),
Multi(Box<MultiSchema>),
Empty(EmptySchema),
Single(Box<SingleSchema>), }
const _: () = assert!(
std::mem::size_of::<Schema>() <= 16,
"Schema variants must stay boxed",
);
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct EmptySchema;
impl Serialize for EmptySchema {
fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
ser.serialize_map(Some(0))?.end()
}
}
impl<'de> Deserialize<'de> for EmptySchema {
fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
struct EmptyVisitor;
impl<'de> serde::de::Visitor<'de> for EmptyVisitor {
type Value = EmptySchema;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("empty schema object `{}`")
}
fn visit_map<M: serde::de::MapAccess<'de>>(
self,
mut map: M,
) -> Result<EmptySchema, M::Error> {
if map.next_key::<serde::de::IgnoredAny>()?.is_some() {
return Err(serde::de::Error::custom(
"expected empty schema object `{}`",
));
}
Ok(EmptySchema)
}
}
de.deserialize_map(EmptyVisitor)
}
}
impl From<EmptySchema> for Schema {
fn from(s: EmptySchema) -> Self {
Schema::Empty(s)
}
}
impl From<bool> for Schema {
fn from(b: bool) -> Self {
Schema::Bool(b)
}
}
impl Default for Schema {
fn default() -> Self {
Schema::Single(Box::default())
}
}
impl From<SingleSchema> for Schema {
fn from(s: SingleSchema) -> Self {
Schema::Single(Box::new(s))
}
}
impl From<MultiSchema> for Schema {
fn from(s: MultiSchema) -> Self {
Schema::Multi(Box::new(s))
}
}
fn schema_from_value(value: serde_json::Value) -> Result<Schema, serde_json::Error> {
enum Route {
Bool(bool),
Empty,
AllOf,
AnyOf,
OneOf,
Not,
Multi,
Single,
}
let route = match &value {
serde_json::Value::Bool(b) => Route::Bool(*b),
serde_json::Value::Object(map) if map.is_empty() => Route::Empty,
serde_json::Value::Object(map) => {
if map.contains_key("allOf") {
Route::AllOf
} else if map.contains_key("anyOf") {
Route::AnyOf
} else if map.contains_key("oneOf") {
Route::OneOf
} else if map.contains_key("not") {
Route::Not
} else if matches!(map.get("type"), Some(serde_json::Value::Array(_))) {
Route::Multi
} else {
Route::Single
}
}
_ => {
return Err(serde::de::Error::custom(
"a Schema must be a JSON object or boolean",
));
}
};
Ok(match route {
Route::Bool(b) => Schema::Bool(b),
Route::Empty => Schema::Empty(EmptySchema::deserialize(value)?),
Route::AllOf => Schema::AllOf(Box::new(AllOfSchema::deserialize(value)?)),
Route::AnyOf => Schema::AnyOf(Box::new(AnyOfSchema::deserialize(value)?)),
Route::OneOf => Schema::OneOf(Box::new(OneOfSchema::deserialize(value)?)),
Route::Not => Schema::Not(Box::new(NotSchema::deserialize(value)?)),
Route::Multi => Schema::Multi(Box::new(MultiSchema::deserialize(value)?)),
Route::Single => Schema::Single(Box::new(single_schema_from_value(value)?)),
})
}
fn single_schema_from_value(value: serde_json::Value) -> Result<SingleSchema, serde_json::Error> {
let schema_type = value
.as_object()
.and_then(|map| map.get("type"))
.and_then(|t| t.as_str());
Ok(match schema_type {
Some("string") => SingleSchema::String(StringSchema::deserialize(value)?),
Some("integer") => SingleSchema::Integer(IntegerSchema::deserialize(value)?),
Some("number") => SingleSchema::Number(NumberSchema::deserialize(value)?),
Some("boolean") => SingleSchema::Boolean(BooleanSchema::deserialize(value)?),
Some("array") => SingleSchema::Array(ArraySchema::deserialize(value)?),
Some("null") => SingleSchema::Null(NullSchema::deserialize(value)?),
_ => SingleSchema::Object(ObjectSchema::deserialize(value)?),
})
}
impl<'de> Deserialize<'de> for Schema {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
schema_from_value(value).map_err(serde::de::Error::custom)
}
}
impl<'de> Deserialize<'de> for SingleSchema {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
single_schema_from_value(value).map_err(serde::de::Error::custom)
}
}
impl From<AllOfSchema> for Schema {
fn from(s: AllOfSchema) -> Self {
Schema::AllOf(Box::new(s))
}
}
impl From<AnyOfSchema> for Schema {
fn from(s: AnyOfSchema) -> Self {
Schema::AnyOf(Box::new(s))
}
}
impl From<OneOfSchema> for Schema {
fn from(s: OneOfSchema) -> Self {
Schema::OneOf(Box::new(s))
}
}
impl From<NotSchema> for Schema {
fn from(s: NotSchema) -> Self {
Schema::Not(Box::new(s))
}
}
impl From<StringSchema> for SingleSchema {
fn from(s: StringSchema) -> Self {
SingleSchema::String(s)
}
}
impl From<IntegerSchema> for SingleSchema {
fn from(s: IntegerSchema) -> Self {
SingleSchema::Integer(s)
}
}
impl From<NumberSchema> for SingleSchema {
fn from(s: NumberSchema) -> Self {
SingleSchema::Number(s)
}
}
impl From<BooleanSchema> for SingleSchema {
fn from(s: BooleanSchema) -> Self {
SingleSchema::Boolean(s)
}
}
impl From<ArraySchema> for SingleSchema {
fn from(s: ArraySchema) -> Self {
SingleSchema::Array(s)
}
}
impl From<NullSchema> for SingleSchema {
fn from(s: NullSchema) -> Self {
SingleSchema::Null(s)
}
}
impl From<ObjectSchema> for SingleSchema {
fn from(s: ObjectSchema) -> Self {
SingleSchema::Object(s)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
pub struct AllOfSchema {
#[serde(rename = "allOf")]
pub all_of: Vec<RefOr<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub discriminator: Option<Discriminator>,
#[serde(flatten)]
#[serde(with = "extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
pub struct AnyOfSchema {
#[serde(rename = "anyOf")]
pub any_of: Vec<RefOr<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub discriminator: Option<Discriminator>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<Vec<serde_json::Value>>,
#[serde(flatten)]
#[serde(with = "extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
pub struct OneOfSchema {
#[serde(rename = "oneOf")]
pub one_of: Vec<RefOr<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub discriminator: Option<Discriminator>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<Vec<serde_json::Value>>,
#[serde(flatten)]
#[serde(with = "extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct NotSchema {
pub not: RefOr<Schema>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<Vec<serde_json::Value>>,
#[serde(flatten)]
#[serde(with = "extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, Serialize, PartialEq)]
#[serde(untagged)]
pub enum SingleSchema {
#[serde(rename = "string")]
String(StringSchema),
#[serde(rename = "integer")]
Integer(IntegerSchema),
#[serde(rename = "number")]
Number(NumberSchema),
#[serde(rename = "boolean")]
Boolean(BooleanSchema),
#[serde(rename = "array")]
Array(ArraySchema),
#[serde(rename = "null")]
Null(NullSchema),
#[serde(rename = "object")]
Object(ObjectSchema), }
impl Default for SingleSchema {
fn default() -> Self {
SingleSchema::Object(ObjectSchema::default())
}
}
impl Display for SingleSchema {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
SingleSchema::String(_) => write!(f, "string"),
SingleSchema::Integer(_) => write!(f, "integer"),
SingleSchema::Number(_) => write!(f, "number"),
SingleSchema::Boolean(_) => write!(f, "boolean"),
SingleSchema::Array(_) => write!(f, "array"),
SingleSchema::Null(_) => write!(f, "null"),
SingleSchema::Object(_) => write!(f, "object"),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct StringSchema {
#[serde(rename = "type")]
pub schema_type: MustBe!("string"),
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<StringFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
#[serde(rename = "enum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub enum_values: Option<Vec<String>>,
#[serde(rename = "maxLength")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_length: Option<u64>,
#[serde(rename = "minLength")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_length: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "readOnly")]
pub read_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "writeOnly")]
pub write_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub xml: Option<XML>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<Vec<serde_json::Value>>,
#[serde(flatten)]
#[serde(with = "extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct IntegerSchema {
#[serde(rename = "type")]
pub schema_type: MustBe!("integer"),
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<IntegerFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<i64>,
#[serde(rename = "enum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub enum_values: Option<Vec<i64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<serde_json::Number>,
#[serde(rename = "exclusiveMinimum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_minimum: Option<serde_json::Number>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<serde_json::Number>,
#[serde(rename = "exclusiveMaximum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_maximum: Option<serde_json::Number>,
#[serde(rename = "multipleOf")]
#[serde(skip_serializing_if = "Option::is_none")]
pub multiple_of: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "readOnly")]
pub read_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "writeOnly")]
pub write_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub xml: Option<XML>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<Vec<serde_json::Value>>,
#[serde(flatten)]
#[serde(with = "extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct NumberSchema {
#[serde(rename = "type")]
pub schema_type: MustBe!("number"),
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<NumberFormat>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<f64>,
#[serde(rename = "enum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub enum_values: Option<Vec<f64>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum: Option<f64>,
#[serde(rename = "exclusiveMinimum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_minimum: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum: Option<f64>,
#[serde(rename = "exclusiveMaximum")]
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusive_maximum: Option<f64>,
#[serde(rename = "multipleOf")]
#[serde(skip_serializing_if = "Option::is_none")]
pub multiple_of: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "readOnly")]
pub read_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "writeOnly")]
pub write_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub xml: Option<XML>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<Vec<serde_json::Value>>,
#[serde(flatten)]
#[serde(with = "extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct BooleanSchema {
#[serde(rename = "type")]
pub schema_type: MustBe!("boolean"),
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "readOnly")]
pub read_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "writeOnly")]
pub write_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub xml: Option<XML>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<Vec<serde_json::Value>>,
#[serde(flatten)]
#[serde(with = "extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct ArraySchema {
#[serde(rename = "type")]
pub schema_type: MustBe!("array"),
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<BoolOr<RefOr<Schema>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<Vec<serde_json::Value>>,
#[serde(rename = "maxItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub max_items: Option<u64>,
#[serde(rename = "minItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub min_items: Option<u64>,
#[serde(rename = "uniqueItems")]
#[serde(skip_serializing_if = "Option::is_none")]
pub unique_items: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "readOnly")]
pub read_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "writeOnly")]
pub write_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub xml: Option<XML>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<Vec<serde_json::Value>>,
#[serde(flatten)]
#[serde(with = "extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct ObjectSchema {
#[serde(rename = "type", default)]
pub schema_type: MustBe!("object"),
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<BTreeMap<String, RefOr<Schema>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern_properties: Option<BTreeMap<String, RefOr<Schema>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<BTreeMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_properties: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_properties: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_properties: Option<BoolOr<RefOr<Schema>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unevaluated_properties: Option<BoolOr<RefOr<Schema>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub property_names: Option<RefOr<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "readOnly")]
pub read_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "writeOnly")]
pub write_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub xml: Option<XML>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<Vec<serde_json::Value>>,
#[serde(flatten)]
#[serde(with = "extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct NullSchema {
#[serde(rename = "type")]
pub schema_type: MustBe!("null"),
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "readOnly")]
pub read_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "writeOnly")]
pub write_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub xml: Option<XML>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<Vec<serde_json::Value>>,
#[serde(flatten)]
#[serde(with = "extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct MultiSchema {
#[serde(rename = "type")]
pub schema_types: Vec<SchemaType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "readOnly")]
pub read_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "writeOnly")]
pub write_only: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "externalDocs")]
pub external_docs: Option<ExternalDocumentation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub example: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<Vec<serde_json::Value>>,
#[serde(flatten)]
#[serde(with = "extensions")]
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<BTreeMap<String, serde_json::Value>>,
}
impl ValidateWithContext<Spec> for Schema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
match self {
Schema::Bool(_) => {}
Schema::Empty(_) => {}
Schema::Single(s) => s.validate_with_context(ctx, path),
Schema::Multi(s) => s.validate_with_context(ctx, path),
Schema::AllOf(s) => {
if s.all_of.is_empty() {
ctx.error(path.clone(), "`allOf` must be a non-empty array");
}
for (i, schema) in s.all_of.iter().enumerate() {
schema.validate_with_context(ctx, format!("{path}.allOf[{i}]"));
}
if let Some(discriminator) = &s.discriminator {
discriminator.validate_with_context(ctx, format!("{path}.discriminator"));
}
}
Schema::AnyOf(s) => {
if s.any_of.is_empty() {
ctx.error(path.clone(), "`anyOf` must be a non-empty array");
}
for (i, schema) in s.any_of.iter().enumerate() {
schema.validate_with_context(ctx, format!("{path}.anyOf[{i}]"));
}
if let Some(discriminator) = &s.discriminator {
discriminator.validate_with_context(ctx, format!("{path}.discriminator"));
}
}
Schema::OneOf(s) => {
if s.one_of.is_empty() {
ctx.error(path.clone(), "`oneOf` must be a non-empty array");
}
for (i, schema) in s.one_of.iter().enumerate() {
schema.validate_with_context(ctx, format!("{path}.oneOf[{i}]"));
}
if let Some(discriminator) = &s.discriminator {
discriminator.validate_with_context(ctx, format!("{path}.discriminator"));
}
}
Schema::Not(s) => {
s.not.validate_with_context(ctx, format!("{path}.not"));
}
}
}
}
impl ValidateWithContext<Spec> for SingleSchema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
let (read_only, write_only) = match self {
SingleSchema::String(s) => (s.read_only, s.write_only),
SingleSchema::Integer(s) => (s.read_only, s.write_only),
SingleSchema::Number(s) => (s.read_only, s.write_only),
SingleSchema::Boolean(s) => (s.read_only, s.write_only),
SingleSchema::Array(s) => (s.read_only, s.write_only),
SingleSchema::Object(s) => (s.read_only, s.write_only),
SingleSchema::Null(s) => (s.read_only, s.write_only),
};
if read_only == Some(true) && write_only == Some(true) {
ctx.error(
path.clone(),
".readOnly and .writeOnly are mutually exclusive",
);
}
match self {
SingleSchema::String(s) => s.validate_with_context(ctx, path),
SingleSchema::Integer(s) => s.validate_with_context(ctx, path),
SingleSchema::Number(s) => s.validate_with_context(ctx, path),
SingleSchema::Boolean(s) => s.validate_with_context(ctx, path),
SingleSchema::Array(s) => s.validate_with_context(ctx, path),
SingleSchema::Object(s) => s.validate_with_context(ctx, path),
SingleSchema::Null(s) => s.validate_with_context(ctx, path),
}
}
}
impl ValidateWithContext<Spec> for StringSchema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if let Some(docs) = &self.external_docs {
docs.validate_with_context(ctx, format!("{path}.externalDocs"));
}
if let Some(xml) = &self.xml {
xml.validate_with_context(ctx, format!("{path}.xml"));
}
if let (Some(min), Some(max)) = (self.min_length, self.max_length)
&& min > max
{
ctx.error(
path.clone(),
format_args!("`minLength` ({min}) must be ≤ `maxLength` ({max})"),
);
}
}
}
impl ValidateWithContext<Spec> for IntegerSchema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if let Some(docs) = &self.external_docs {
docs.validate_with_context(ctx, format!("{path}.externalDocs"));
}
if let Some(xml) = &self.xml {
xml.validate_with_context(ctx, format!("{path}.xml"));
}
if let Some(m) = self.multiple_of
&& m <= 0.0
{
ctx.error(path.clone(), format_args!("`multipleOf` ({m}) must be > 0"));
}
}
}
impl ValidateWithContext<Spec> for NumberSchema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if let Some(docs) = &self.external_docs {
docs.validate_with_context(ctx, format!("{path}.externalDocs"));
}
if let Some(xml) = &self.xml {
xml.validate_with_context(ctx, format!("{path}.xml"));
}
if let Some(m) = self.multiple_of
&& m <= 0.0
{
ctx.error(path.clone(), format_args!("`multipleOf` ({m}) must be > 0"));
}
}
}
impl ValidateWithContext<Spec> for BooleanSchema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if let Some(docs) = &self.external_docs {
docs.validate_with_context(ctx, format!("{path}.externalDocs"));
}
if let Some(xml) = &self.xml {
xml.validate_with_context(ctx, format!("{path}.xml"));
}
}
}
impl ValidateWithContext<Spec> for ArraySchema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if let Some(docs) = &self.external_docs {
docs.validate_with_context(ctx, format!("{path}.externalDocs"));
}
if let Some(xml) = &self.xml {
xml.validate_with_context(ctx, format!("{path}.xml"));
}
if let Some(items) = &self.items {
items.validate_with_context(ctx, format!("{path}.items"));
}
if let (Some(min), Some(max)) = (self.min_items, self.max_items)
&& min > max
{
ctx.error(
path.clone(),
format_args!("`minItems` ({min}) must be ≤ `maxItems` ({max})"),
);
}
}
}
impl ValidateWithContext<Spec> for ObjectSchema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if let Some(docs) = &self.external_docs {
docs.validate_with_context(ctx, format!("{path}.externalDocs"));
}
if let Some(xml) = &self.xml {
xml.validate_with_context(ctx, format!("{path}.xml"));
}
if let (Some(min), Some(max)) = (self.min_properties, self.max_properties)
&& min > max
{
ctx.error(
path.clone(),
format_args!("`minProperties` ({min}) must be ≤ `maxProperties` ({max})"),
);
}
if let Some(properties) = &self.properties {
for (name, schema) in properties {
schema.validate_with_context(ctx, format!("{path}.properties.{name}"));
}
}
if let Some(properties) = &self.pattern_properties {
for (pattern, schema) in properties {
let path = format!("{path}.pattern_properties[{pattern}]");
schema.validate_with_context(ctx, path.clone());
validate_pattern(pattern, ctx, path);
}
}
if let Some(additional_properties) = &self.additional_properties {
match additional_properties {
BoolOr::Bool(_) => {}
BoolOr::Item(schema) => {
schema.validate_with_context(ctx, format!("{path}.additionalProperties"));
}
}
}
if let Some(unevaluated_properties) = &self.unevaluated_properties {
match unevaluated_properties {
BoolOr::Bool(_) => {}
BoolOr::Item(schema) => {
schema.validate_with_context(ctx, format!("{path}.unevaluatedProperties"));
}
}
}
if let Some(property_names) = &self.property_names {
property_names.validate_with_context(ctx, format!("{path}.propertyNames"));
}
}
}
impl ValidateWithContext<Spec> for NullSchema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if let Some(docs) = &self.external_docs {
docs.validate_with_context(ctx, format!("{path}.externalDocs"));
}
if let Some(xml) = &self.xml {
xml.validate_with_context(ctx, format!("{path}.xml"));
}
}
}
impl ValidateWithContext<Spec> for MultiSchema {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
if self.schema_types.is_empty() {
ctx.error(format!("{path}.type"), "must contain at least one element");
}
let mut unique_types: HashSet<&SchemaType> =
HashSet::with_capacity(self.schema_types.len());
self.schema_types.iter().for_each(|t| {
if let SchemaType::Custom(name) = t {
ctx.error(
format!("{path}.type"),
format_args!("type `{name}` is not supported"),
);
}
if !unique_types.insert(t) {
ctx.error(
format!("{path}.type"),
format_args!("type `{t}` is not unique"),
);
}
});
if let Some(docs) = &self.external_docs {
docs.validate_with_context(ctx, format!("{path}.externalDocs"));
}
}
}
mod extensions {
use std::collections::BTreeMap;
use std::fmt;
use serde::de::{Error, MapAccess, Visitor};
use serde::ser::SerializeMap;
use serde::{Deserializer, Serialize, Serializer};
pub(super) fn deserialize<'de, D>(
deserializer: D,
) -> Result<Option<BTreeMap<String, serde_json::Value>>, D::Error>
where
D: Deserializer<'de>,
{
struct ExtensionsVisitor;
impl<'de> Visitor<'de> for ExtensionsVisitor {
type Value = BTreeMap<String, serde_json::Value>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("extensions: Option<BTreeMap<String, serde_json::Value>>")
}
fn visit_map<V>(
self,
mut map: V,
) -> Result<BTreeMap<String, serde_json::Value>, V::Error>
where
V: MapAccess<'de>,
{
let mut ext: BTreeMap<String, serde_json::Value> = BTreeMap::new();
while let Some(key) = map.next_key::<String>()? {
if ext.contains_key(key.as_str()) {
return Err(Error::custom(format_args!("duplicate field `{key}`")));
}
let value: serde_json::Value = map.next_value()?;
ext.insert(key, value);
}
Ok(ext)
}
}
let map = deserializer.deserialize_map(ExtensionsVisitor)?;
Ok(if map.is_empty() { None } else { Some(map) })
}
pub(super) fn serialize<S>(
ext: &Option<BTreeMap<String, serde_json::Value>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(ext) = ext {
let mut map = serializer.serialize_map(Some(ext.len()))?;
for (k, v) in ext.clone() {
map.serialize_entry(&k, &v)?;
}
map.end()
} else {
None::<BTreeMap<String, serde_json::Value>>.serialize(serializer)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validation::ValidationErrorsExt;
#[test]
fn test_single_deserialize() {
let spec = serde_json::from_value::<Schema>(serde_json::json!({
"type": "string",
"title": "foo",
}))
.unwrap();
if let Schema::Single(o) = &spec {
if let SingleSchema::String(string) = &**o {
assert_eq!(string.title, Some("foo".to_owned()));
} else {
panic!("expected StringSchema");
}
} else {
panic!("expected Single");
}
assert_eq!(
spec,
Schema::Single(Box::new(SingleSchema::String(StringSchema {
title: Some("foo".to_owned()),
..Default::default()
}))),
);
}
#[test]
fn schema_without_type_parses_as_object() {
let json = serde_json::json!({
"title": "Untyped",
"properties": {
"name": {"type": "string"}
},
"required": ["name"]
});
let parsed: Schema = serde_json::from_value(json).expect("must parse");
match &parsed {
Schema::Single(s) => match s.as_ref() {
SingleSchema::Object(o) => {
assert_eq!(o.title.as_deref(), Some("Untyped"));
assert_eq!(o.required.as_deref(), Some(&["name".to_owned()][..]));
assert!(o.properties.is_some());
}
other => panic!("expected Object, got {other:?}"),
},
_ => panic!("expected Schema::Single, got {parsed:?}"),
}
let parsed: Schema = serde_json::from_value(serde_json::json!({})).expect("must parse");
assert_eq!(parsed, Schema::Empty(EmptySchema));
}
#[test]
fn schema_typed_string_still_dispatches_correctly_v31() {
let parsed: Schema =
serde_json::from_value(serde_json::json!({"type": "string"})).expect("must parse");
match parsed {
Schema::Single(s) => match *s {
SingleSchema::String(_) => {}
other => panic!("expected String, got {other:?}"),
},
_ => panic!("expected Schema::Single"),
}
}
#[test]
fn schema_with_type_array_still_routes_to_multi() {
let parsed: Schema =
serde_json::from_value(serde_json::json!({"type": ["string", "null"]}))
.expect("must parse");
assert!(
matches!(parsed, Schema::Multi(_)),
"expected Schema::Multi, got {parsed:?}"
);
}
#[test]
fn test_single_serialize() {
assert_eq!(
serde_json::to_value(Schema::from(SingleSchema::from(StringSchema {
title: Some("foo".to_owned()),
..Default::default()
})))
.unwrap(),
serde_json::json!({
"type": "string",
"title": "foo",
}),
);
assert_eq!(
serde_json::to_value(Schema::from(SingleSchema::from(ObjectSchema {
title: Some("foo".to_owned()),
required: Some(vec!["bar".to_owned()]),
properties: Some({
let mut map = BTreeMap::new();
map.insert(
"bar".to_owned(),
RefOr::new_item(Schema::from(SingleSchema::from(StringSchema {
title: Some("foo bar".to_owned()),
..Default::default()
}))),
);
map
}),
..Default::default()
})))
.unwrap(),
serde_json::json!({
"type": "object",
"title": "foo",
"required": ["bar"],
"properties": {
"bar": {
"type": "string",
"title": "foo bar",
},
},
}),
);
}
#[test]
fn test_all_of_deserialize() {
let spec = serde_json::from_value::<Schema>(serde_json::json!({
"allOf": [
{
"$ref": "#/definitions/bar"
},
{
"type": "object",
"title": "foo",
},
],
}))
.unwrap();
if let Schema::AllOf(schema) = &spec {
assert_eq!(schema.all_of.len(), 2);
match schema.all_of[0].clone() {
RefOr::Ref(r) => {
assert_eq!(r.reference, "#/definitions/bar".to_owned());
}
_ => panic!("expected Ref"),
}
match schema.all_of[1].clone() {
RefOr::Item(o) => {
if let Schema::Single(o) = o {
if let SingleSchema::Object(o) = *o {
assert_eq!(o.title, Some("foo".to_owned()));
} else {
panic!("expected ObjectSchema");
}
} else {
panic!("expected Single");
}
}
_ => panic!("expected Schema"),
}
} else {
panic!("expected AllOf schema, but got {spec:?}");
}
}
#[test]
fn composition_keyword_beside_sibling_still_routes_to_allof() {
let s: Schema = serde_json::from_value(serde_json::json!({
"allOf": [{"$ref": "#/components/schemas/Base"}],
"type": "object",
}))
.unwrap();
assert!(matches!(s, Schema::AllOf(_)), "got {s:?}");
let s: Schema = serde_json::from_value(serde_json::json!({
"allOf": [{"$ref": "#/components/schemas/Base"}],
"anyOf": [{"$ref": "#/components/schemas/Other"}],
}))
.unwrap();
assert!(matches!(s, Schema::AllOf(_)), "got {s:?}");
}
#[test]
fn test_all_of_serialize() {
assert_eq!(
serde_json::to_value(Schema::from(AllOfSchema {
all_of: vec![
RefOr::new_ref("#/definitions/bar".to_owned()),
RefOr::new_item(Schema::from(SingleSchema::from(ObjectSchema {
title: Some("foo".to_owned()),
..Default::default()
}))),
],
..Default::default()
}))
.unwrap(),
serde_json::json!({
"allOf": [
{
"$ref": "#/definitions/bar"
},
{
"type": "object",
"title": "foo",
},
],
}),
);
}
#[test]
fn test_string_serialize_deserialize() {
let spec = Schema::Single(Box::new(SingleSchema::String(StringSchema {
title: Some("foo".to_string()),
format: Some(StringFormat::Custom("custom".to_string())),
default: Some("d".to_string()),
enum_values: Some(vec!["a".to_string(), "b".to_string(), "d".to_string()]),
max_length: Some(1),
min_length: Some(1),
examples: Some(vec![serde_json::json!("a"), serde_json::json!("b")]),
..Default::default()
})));
let value = serde_json::json!({
"type": "string",
"title": "foo",
"format": "custom",
"default": "d",
"enum": ["a", "b", "d"],
"maxLength": 1,
"minLength": 1,
"examples": ["a", "b"],
});
assert_eq!(serde_json::to_value(&spec).unwrap(), value);
assert_eq!(serde_json::from_value::<Schema>(value).unwrap(), spec);
}
#[test]
fn test_integer_serialize_deserialize() {
let spec = Schema::Single(Box::new(SingleSchema::Integer(IntegerSchema {
title: Some("foo".to_string()),
format: Some(IntegerFormat::Int32),
default: Some(42),
enum_values: Some(vec![1, 42, 105]),
minimum: Some(1.into()),
maximum: Some(105.into()),
examples: Some(vec![serde_json::json!(1), serde_json::json!(42)]),
..Default::default()
})));
let value = serde_json::json!({
"type": "integer",
"title": "foo",
"format": "int32",
"default": 42,
"enum": [1, 42, 105],
"minimum": 1,
"maximum": 105,
"examples": [1, 42],
});
assert_eq!(serde_json::to_value(&spec).unwrap(), value);
assert_eq!(serde_json::from_value::<Schema>(value).unwrap(), spec);
}
#[test]
fn test_numeric_bounds_are_numbers() {
let json = serde_json::json!({
"type": "integer",
"exclusiveMinimum": 0,
"exclusiveMaximum": 100,
});
let parsed: Schema = serde_json::from_value(json.clone()).expect("must parse");
match &parsed {
Schema::Single(s) => match s.as_ref() {
SingleSchema::Integer(int) => {
assert_eq!(int.exclusive_minimum.as_ref().unwrap().as_i64(), Some(0));
assert_eq!(int.exclusive_maximum.as_ref().unwrap().as_i64(), Some(100));
}
_ => panic!("expected Integer schema"),
},
_ => panic!("expected Single schema"),
}
assert_eq!(serde_json::to_value(&parsed).unwrap(), json);
let json = serde_json::json!({
"type": "integer",
"minimum": 0.5,
"maximum": 99.5,
"exclusiveMinimum": 0.5,
"exclusiveMaximum": 99.5,
});
let parsed: Schema = serde_json::from_value(json.clone()).expect("must parse");
match &parsed {
Schema::Single(s) => match s.as_ref() {
SingleSchema::Integer(int) => {
assert_eq!(int.minimum.as_ref().unwrap().as_f64(), Some(0.5));
assert_eq!(int.maximum.as_ref().unwrap().as_f64(), Some(99.5));
assert_eq!(int.exclusive_minimum.as_ref().unwrap().as_f64(), Some(0.5));
assert_eq!(int.exclusive_maximum.as_ref().unwrap().as_f64(), Some(99.5));
}
_ => panic!("expected Integer schema"),
},
_ => panic!("expected Single schema"),
}
assert_eq!(serde_json::to_value(&parsed).unwrap(), json);
let json = serde_json::json!({
"type": "number",
"exclusiveMinimum": 0.5,
"exclusiveMaximum": 1.5,
});
let parsed: Schema = serde_json::from_value(json.clone()).expect("must parse");
match &parsed {
Schema::Single(s) => match s.as_ref() {
SingleSchema::Number(n) => {
assert_eq!(n.exclusive_minimum, Some(0.5));
assert_eq!(n.exclusive_maximum, Some(1.5));
}
_ => panic!("expected Number schema"),
},
_ => panic!("expected Single schema"),
}
assert_eq!(serde_json::to_value(&parsed).unwrap(), json);
}
#[test]
fn test_number_serialize_deserialize() {
let spec = Schema::Single(Box::new(SingleSchema::Number(NumberSchema {
title: Some("foo".to_string()),
format: Some(NumberFormat::Float),
default: Some(42.0),
enum_values: Some(vec![1.0, 42.0, 105.0]),
minimum: Some(1.0),
maximum: Some(105.0),
examples: Some(vec![serde_json::json!(1.0), serde_json::json!(42.0)]),
..Default::default()
})));
let value = serde_json::json!({
"type": "number",
"title": "foo",
"format": "float",
"default": 42.0,
"enum": [1.0, 42.0, 105.0],
"minimum": 1.0,
"maximum": 105.0,
"examples": [1.0, 42.0],
});
assert_eq!(serde_json::to_value(&spec).unwrap(), value);
assert_eq!(serde_json::from_value::<Schema>(value).unwrap(), spec);
}
#[test]
fn test_boolean_serialize_deserialize() {
let spec = Schema::Single(Box::new(SingleSchema::Boolean(BooleanSchema {
title: Some("foo".to_string()),
default: Some(false),
examples: Some(vec![serde_json::json!(true), serde_json::json!(false)]),
..Default::default()
})));
let value = serde_json::json!({
"type": "boolean",
"title": "foo",
"default": false,
"examples": [true, false],
});
assert_eq!(serde_json::to_value(&spec).unwrap(), value);
assert_eq!(serde_json::from_value::<Schema>(value).unwrap(), spec);
}
#[test]
fn test_array_serialize_deserialize() {
let spec = Schema::from(SingleSchema::from(ArraySchema {
title: Some("foo".to_string()),
items: Some(BoolOr::Item(RefOr::new_item(Schema::from(
SingleSchema::from(IntegerSchema {
title: Some("bar".into()),
..Default::default()
}),
)))),
default: Some(vec![
serde_json::json!(1),
serde_json::json!(2),
serde_json::json!(3),
]),
examples: Some(vec![
serde_json::json!([1, 42, 105]),
serde_json::json!([0, 25, 43]),
]),
..Default::default()
}));
let value = serde_json::json!({
"type": "array",
"title": "foo",
"items": {
"type": "integer",
"title": "bar",
},
"default": [1, 2, 3],
"examples": [[1, 42, 105], [0, 25, 43]],
});
assert_eq!(serde_json::to_value(&spec).unwrap(), value);
assert_eq!(serde_json::from_value::<Schema>(value).unwrap(), spec);
}
#[test]
fn test_object_serialize_deserialize() {
let spec = Schema::Single(Box::new(SingleSchema::Object(ObjectSchema {
title: Some("foo".to_string()),
properties: Some(BTreeMap::from_iter(vec![(
"bar".into(),
RefOr::new_item(Schema::from(
SingleSchema::Integer(IntegerSchema::default()),
)),
)])),
default: Some(BTreeMap::from_iter(vec![(
"bar".into(),
serde_json::json!(42),
)])),
examples: Some(vec![
serde_json::json!({"bar": 42}),
serde_json::json!({"bar": 105}),
]),
..Default::default()
})));
let value = serde_json::json!({
"type": "object",
"title": "foo",
"properties": {
"bar": {
"type": "integer",
},
},
"default": {"bar": 42},
"examples": [{"bar": 42}, {"bar": 105}],
});
assert_eq!(serde_json::to_value(&spec).unwrap(), value);
assert_eq!(serde_json::from_value::<Schema>(value).unwrap(), spec);
}
#[test]
fn test_null_serialize_deserialize() {
let spec = Schema::Single(Box::new(SingleSchema::Null(NullSchema {
title: Some("foo".to_string()),
examples: Some(vec![serde_json::json!(null)]),
..Default::default()
})));
let value = serde_json::json!({
"type": "null",
"title": "foo",
"examples": [null],
});
assert_eq!(serde_json::to_value(&spec).unwrap(), value);
assert_eq!(serde_json::from_value::<Schema>(value).unwrap(), spec);
}
#[test]
fn test_multi_serialize_deserialize() {
let spec = Schema::Multi(Box::new(MultiSchema {
schema_types: vec![SchemaType::String, SchemaType::Integer],
title: Some("foo".to_string()),
examples: Some(vec![serde_json::json!("bar"), serde_json::json!(42)]),
..Default::default()
}));
let value = serde_json::json!({
"type": ["string", "integer"],
"title": "foo",
"examples": ["bar", 42],
});
assert_eq!(serde_json::to_value(&spec).unwrap(), value);
assert_eq!(serde_json::from_value::<Schema>(value).unwrap(), spec);
let spec_owner = Spec::default();
let mut ctx = Context::new(&spec_owner, crate::validation::Options::empty());
let multi = MultiSchema {
schema_types: vec![
SchemaType::String,
SchemaType::Integer,
SchemaType::Number,
SchemaType::Boolean,
SchemaType::Object,
SchemaType::Array,
SchemaType::Null,
],
..Default::default()
};
multi.validate_with_context(&mut ctx, "schema".into());
assert!(
ctx.errors.is_empty(),
"all primitive types should be valid: {:?}",
ctx.errors
);
}
#[test]
fn test_one_of_serialize_deserialize() {
let spec = Schema::OneOf(Box::new(OneOfSchema {
one_of: vec![
RefOr::new_ref("#/components/schemas/Cat"),
RefOr::new_ref("#/components/schemas/Dog"),
RefOr::new_ref("#/components/schemas/Lizard"),
],
discriminator: Some(Discriminator {
property_name: "petType".into(),
..Default::default()
}),
..Default::default()
}));
let value = serde_json::json!({
"oneOf": [
{"$ref": "#/components/schemas/Cat"},
{"$ref": "#/components/schemas/Dog"},
{"$ref": "#/components/schemas/Lizard"},
],
"discriminator": {
"propertyName": "petType",
}
});
assert_eq!(serde_json::to_value(&spec).unwrap(), value);
assert_eq!(serde_json::from_value::<Schema>(value).unwrap(), spec);
}
#[test]
fn test_any_of_serialize_deserialize() {
let spec = Schema::AnyOf(Box::new(AnyOfSchema {
any_of: vec![
RefOr::new_ref("#/components/schemas/Cat"),
RefOr::new_ref("#/components/schemas/Dog"),
RefOr::new_ref("#/components/schemas/Lizard"),
],
discriminator: Some(Discriminator {
property_name: "petType".into(),
..Default::default()
}),
..Default::default()
}));
let value = serde_json::json!({
"anyOf": [
{"$ref": "#/components/schemas/Cat"},
{"$ref": "#/components/schemas/Dog"},
{"$ref": "#/components/schemas/Lizard"},
],
"discriminator": {
"propertyName": "petType",
}
});
assert_eq!(serde_json::to_value(&spec).unwrap(), value);
assert_eq!(serde_json::from_value::<Schema>(value).unwrap(), spec);
}
#[test]
fn test_all_of_serialize_deserialize() {
let spec = Schema::AllOf(Box::new(AllOfSchema {
all_of: vec![
RefOr::new_ref("#/components/schemas/Cat"),
RefOr::new_ref("#/components/schemas/Dog"),
RefOr::new_ref("#/components/schemas/Lizard"),
],
discriminator: Some(Discriminator {
property_name: "petType".into(),
..Default::default()
}),
..Default::default()
}));
let value = serde_json::json!({
"allOf": [
{"$ref": "#/components/schemas/Cat"},
{"$ref": "#/components/schemas/Dog"},
{"$ref": "#/components/schemas/Lizard"},
],
"discriminator": {
"propertyName": "petType",
}
});
assert_eq!(serde_json::to_value(&spec).unwrap(), value);
assert_eq!(serde_json::from_value::<Schema>(value).unwrap(), spec);
}
#[test]
fn boolean_schema_true_and_false_round_trip() {
let t: Schema = serde_json::from_value(serde_json::json!(true)).unwrap();
assert!(matches!(t, Schema::Bool(true)));
assert_eq!(serde_json::to_value(&t).unwrap(), serde_json::json!(true));
let f: Schema = serde_json::from_value(serde_json::json!(false)).unwrap();
assert!(matches!(f, Schema::Bool(false)));
assert_eq!(serde_json::to_value(&f).unwrap(), serde_json::json!(false));
let spec = crate::v3_2::spec::Spec::default();
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
Schema::Bool(true).validate_with_context(&mut ctx, "s".into());
assert!(ctx.errors.is_empty(), "errors: {:?}", ctx.errors);
}
#[test]
fn schema_from_bool_helper() {
let _: Schema = true.into();
let _: Schema = false.into();
}
#[test]
fn from_conversions_each_variant() {
let _: Schema = SingleSchema::from(StringSchema::default()).into();
let _: Schema = SingleSchema::from(IntegerSchema::default()).into();
let _: Schema = SingleSchema::from(NumberSchema::default()).into();
let _: Schema = SingleSchema::from(BooleanSchema::default()).into();
let _: Schema = SingleSchema::from(ArraySchema::default()).into();
let _: Schema = SingleSchema::from(ObjectSchema::default()).into();
let _: Schema = SingleSchema::from(NullSchema::default()).into();
let _: Schema = AllOfSchema::default().into();
let _: Schema = AnyOfSchema::default().into();
let _: Schema = OneOfSchema::default().into();
let _: Schema = NotSchema {
not: RefOr::new_item(Schema::default()),
external_docs: None,
example: None,
examples: None,
extensions: None,
}
.into();
let _: Schema = MultiSchema::default().into();
assert!(matches!(Schema::default(), Schema::Single(_)));
assert!(matches!(SingleSchema::default(), SingleSchema::Object(_)));
}
#[test]
fn single_schema_display_each_variant() {
assert_eq!(
SingleSchema::String(StringSchema::default()).to_string(),
"string"
);
assert_eq!(
SingleSchema::Integer(IntegerSchema::default()).to_string(),
"integer"
);
assert_eq!(
SingleSchema::Number(NumberSchema::default()).to_string(),
"number"
);
assert_eq!(
SingleSchema::Boolean(BooleanSchema::default()).to_string(),
"boolean"
);
assert_eq!(
SingleSchema::Array(ArraySchema::default()).to_string(),
"array"
);
assert_eq!(
SingleSchema::Object(ObjectSchema::default()).to_string(),
"object"
);
assert_eq!(
SingleSchema::Null(NullSchema::default()).to_string(),
"null"
);
}
#[test]
fn composition_validate_dispatches_with_discriminator() {
let spec = crate::v3_2::spec::Spec::default();
let bad_disc = || crate::v3_2::discriminator::Discriminator::default();
for s in [
Schema::AllOf(Box::new(AllOfSchema {
all_of: vec![],
discriminator: Some(bad_disc()),
..Default::default()
})),
Schema::AnyOf(Box::new(AnyOfSchema {
any_of: vec![],
discriminator: Some(bad_disc()),
..Default::default()
})),
Schema::OneOf(Box::new(OneOfSchema {
one_of: vec![],
discriminator: Some(bad_disc()),
..Default::default()
})),
] {
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "s".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("propertyName") && e.contains("must not be empty")),
"expected discriminator empty-propertyName error: {:?}",
ctx.errors
);
}
}
#[test]
fn boolean_and_null_variants_validate() {
let spec = crate::v3_2::spec::Spec::default();
let bad_docs = || crate::v3_2::external_documentation::ExternalDocumentation {
url: "".into(),
description: None,
extensions: None,
};
for s in [
Schema::Single(Box::new(SingleSchema::Boolean(BooleanSchema {
external_docs: Some(bad_docs()),
..Default::default()
}))),
Schema::Single(Box::new(SingleSchema::Null(NullSchema {
external_docs: Some(bad_docs()),
..Default::default()
}))),
] {
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "s".into());
assert!(
ctx.errors.mentions("externalDocs.url"),
"expected externalDocs walk: {:?}",
ctx.errors
);
}
}
#[test]
fn keyword_consistency_violations_reported() {
let spec = crate::v3_2::spec::Spec::default();
let cases: Vec<(&str, Schema, &str)> = vec![
(
"string min/max",
Schema::Single(Box::new(SingleSchema::String(StringSchema {
min_length: Some(10),
max_length: Some(5),
..Default::default()
}))),
"minLength",
),
(
"integer multipleOf <= 0",
Schema::Single(Box::new(SingleSchema::Integer(IntegerSchema {
multiple_of: Some(0.0),
..Default::default()
}))),
"multipleOf",
),
(
"number multipleOf < 0",
Schema::Single(Box::new(SingleSchema::Number(NumberSchema {
multiple_of: Some(-1.0),
..Default::default()
}))),
"multipleOf",
),
(
"array min/max items",
Schema::Single(Box::new(SingleSchema::Array(ArraySchema {
min_items: Some(10),
max_items: Some(5),
..Default::default()
}))),
"minItems",
),
(
"object min/max properties",
Schema::Single(Box::new(SingleSchema::Object(ObjectSchema {
min_properties: Some(10),
max_properties: Some(5),
..Default::default()
}))),
"minProperties",
),
];
for (label, schema, needle) in cases {
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
schema.validate_with_context(&mut ctx, "s".into());
assert!(
ctx.errors.mentions(needle),
"case `{label}`: expected `{needle}` error: {:?}",
ctx.errors
);
}
}
#[test]
fn read_only_write_only_mutex() {
let json = serde_json::json!({
"type": "string",
"readOnly": true,
"writeOnly": true,
});
let s: Schema = serde_json::from_value(json).unwrap();
let spec = crate::v3_2::spec::Spec::default();
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "s".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("readOnly and .writeOnly are mutually exclusive")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn composition_arrays_must_be_non_empty() {
let spec = crate::v3_2::spec::Spec::default();
for (json, kw) in [
(serde_json::json!({"allOf": []}), "allOf"),
(serde_json::json!({"anyOf": []}), "anyOf"),
(serde_json::json!({"oneOf": []}), "oneOf"),
] {
let s: Schema = serde_json::from_value(json).unwrap();
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "s".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains(&format!("`{kw}` must be a non-empty array"))),
"{kw} errors: {:?}",
ctx.errors
);
}
}
#[test]
fn multi_schema_type_array_must_be_non_empty() {
let spec = crate::v3_2::spec::Spec::default();
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
let m = MultiSchema {
schema_types: vec![],
..Default::default()
};
let s = Schema::Multi(Box::new(m));
s.validate_with_context(&mut ctx, "s".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("s.type") && e.contains("must contain at least one element")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn common_codegen_extensions_round_trip_via_generic_extensions() {
let enum_json = serde_json::json!({
"type": "string",
"enum": ["open", "closed"],
"x-enumDescriptions": ["Open state", "Closed state"]
});
let schema: Schema = serde_json::from_value(enum_json.clone()).unwrap();
assert_eq!(serde_json::to_value(&schema).unwrap(), enum_json);
let spec = crate::v3_2::spec::Spec::default();
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
schema.validate_with_context(&mut ctx, "s".into());
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
let object_json = serde_json::json!({
"type": "object",
"x-tags": ["models"],
"x-discriminator-value": "pet"
});
let schema: Schema = serde_json::from_value(object_json.clone()).unwrap();
assert_eq!(serde_json::to_value(&schema).unwrap(), object_json);
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
schema.validate_with_context(&mut ctx, "s".into());
assert!(ctx.errors.is_empty(), "no errors: {:?}", ctx.errors);
}
#[test]
fn empty_schema_default_is_unit_value() {
assert_eq!(EmptySchema, <EmptySchema as Default>::default());
}
#[test]
fn empty_schema_serializes_as_empty_object() {
let s = serde_json::to_string(&EmptySchema).unwrap();
assert_eq!(s, "{}");
let v = serde_json::to_value(EmptySchema).unwrap();
assert!(v.is_object());
assert!(v.as_object().unwrap().is_empty());
}
#[test]
fn empty_schema_deserializes_from_empty_object() {
let from_str: EmptySchema = serde_json::from_str("{}").unwrap();
assert_eq!(from_str, EmptySchema);
let from_value: EmptySchema = serde_json::from_value(serde_json::json!({})).unwrap();
assert_eq!(from_value, EmptySchema);
}
#[test]
fn empty_schema_rejects_populated_object() {
let err = serde_json::from_value::<EmptySchema>(serde_json::json!({"k": 1}))
.expect_err("populated object must reject");
assert!(
err.to_string().contains("expected empty schema object"),
"error must explain the constraint: {err}"
);
let err = serde_json::from_value::<EmptySchema>(serde_json::json!({"x": null}))
.expect_err("single null entry must reject");
assert!(err.to_string().contains("expected empty schema object"));
}
#[test]
fn empty_schema_rejects_non_object_shapes() {
for value in [
serde_json::json!(null),
serde_json::json!(true),
serde_json::json!(false),
serde_json::json!(0),
serde_json::json!("{}"),
serde_json::json!([]),
] {
assert!(
serde_json::from_value::<EmptySchema>(value.clone()).is_err(),
"{value} must not deserialize as EmptySchema",
);
}
}
#[test]
fn schema_from_empty_schema_dispatches_to_empty_variant() {
let schema: Schema = EmptySchema.into();
assert_eq!(schema, Schema::Empty(EmptySchema));
let schema: Schema = Schema::from(EmptySchema);
assert_eq!(schema, Schema::Empty(EmptySchema));
assert_eq!(
serde_json::to_value(&schema).unwrap(),
serde_json::json!({})
);
}
#[test]
fn empty_schema_round_trip_via_string_is_stable() {
let original = EmptySchema;
let encoded = serde_json::to_string(&original).unwrap();
let decoded: EmptySchema = serde_json::from_str(&encoded).unwrap();
assert_eq!(original, decoded);
assert_eq!(serde_json::to_string(&decoded).unwrap(), encoded);
}
#[test]
fn empty_schema_round_trips_as_literal_empty_object() {
let json = serde_json::json!({});
let schema: Schema = serde_json::from_value(json.clone()).unwrap();
assert_eq!(schema, Schema::Empty(EmptySchema));
assert_eq!(serde_json::to_value(&schema).unwrap(), json);
}
#[test]
fn schema_with_only_description_remains_object() {
let json = serde_json::json!({"description": "just metadata"});
let schema: Schema = serde_json::from_value(json).unwrap();
match &schema {
Schema::Single(s) => match s.as_ref() {
SingleSchema::Object(o) => {
assert_eq!(o.description.as_deref(), Some("just metadata"));
}
other => panic!("expected ObjectSchema, got {other}"),
},
other => panic!("expected Single, got {other:?}"),
}
let value = serde_json::to_value(&schema).unwrap();
assert_eq!(value["type"], "object");
assert_eq!(value["description"], "just metadata");
}
#[test]
fn explicit_object_schema_still_picks_object_variant() {
let json = serde_json::json!({"type": "object", "title": "T"});
let schema: Schema = serde_json::from_value(json.clone()).unwrap();
let Schema::Single(s) = &schema else {
panic!("expected Single, got {schema:?}");
};
let SingleSchema::Object(_) = s.as_ref() else {
panic!("expected ObjectSchema, got {s}");
};
assert_eq!(serde_json::to_value(&schema).unwrap(), json);
}
#[test]
fn empty_schema_validates_clean() {
let schema = Schema::Empty(EmptySchema);
let spec = crate::v3_2::spec::Spec::default();
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
schema.validate_with_context(&mut ctx, "s".into());
assert!(ctx.errors.is_empty(), "errors: {:?}", ctx.errors);
}
#[test]
fn empty_schema_distinct_from_bool_true() {
let bool_schema: Schema = serde_json::from_value(serde_json::json!(true)).unwrap();
let empty_schema: Schema = serde_json::from_value(serde_json::json!({})).unwrap();
assert_eq!(bool_schema, Schema::Bool(true));
assert_eq!(empty_schema, Schema::Empty(EmptySchema));
assert_ne!(bool_schema, empty_schema);
}
#[test]
fn any_of_iterates_and_validates_each_element() {
let spec = crate::v3_2::spec::Spec::default();
let bad_docs = crate::v3_2::external_documentation::ExternalDocumentation {
url: "".into(), description: None,
extensions: None,
};
let s = Schema::AnyOf(Box::new(AnyOfSchema {
any_of: vec![
RefOr::new_item(Schema::Single(Box::new(SingleSchema::String(
StringSchema {
external_docs: Some(bad_docs.clone()),
..Default::default()
},
)))),
RefOr::new_item(Schema::Single(Box::new(SingleSchema::Integer(
IntegerSchema {
external_docs: Some(bad_docs.clone()),
..Default::default()
},
)))),
],
..Default::default()
}));
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "s".into());
assert!(
ctx.errors.len() >= 2,
"expected at least 2 errors from AnyOf child iteration: {:?}",
ctx.errors
);
assert!(
ctx.errors.mentions("anyOf[0]") && ctx.errors.mentions("anyOf[1]"),
"errors should mention both indices: {:?}",
ctx.errors
);
}
#[test]
fn one_of_iterates_and_validates_each_element() {
let spec = crate::v3_2::spec::Spec::default();
let bad_docs = crate::v3_2::external_documentation::ExternalDocumentation {
url: "".into(),
description: None,
extensions: None,
};
let s = Schema::OneOf(Box::new(OneOfSchema {
one_of: vec![RefOr::new_item(Schema::Single(Box::new(
SingleSchema::Number(NumberSchema {
external_docs: Some(bad_docs.clone()),
..Default::default()
}),
)))],
..Default::default()
}));
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "s".into());
assert!(
ctx.errors.mentions("oneOf[0]"),
"error should mention oneOf[0]: {:?}",
ctx.errors
);
}
#[test]
fn not_schema_validates_inner_schema() {
let spec = crate::v3_2::spec::Spec::default();
let bad_docs = crate::v3_2::external_documentation::ExternalDocumentation {
url: "".into(),
description: None,
extensions: None,
};
let s = Schema::Not(Box::new(NotSchema {
not: RefOr::new_item(Schema::Single(Box::new(SingleSchema::String(
StringSchema {
external_docs: Some(bad_docs),
..Default::default()
},
)))),
external_docs: None,
example: None,
examples: None,
extensions: None,
}));
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "s".into());
assert!(
ctx.errors.mentions("not"),
"error should mention not path: {:?}",
ctx.errors
);
}
#[test]
fn string_schema_external_docs_and_xml_are_walked() {
let spec = crate::v3_2::spec::Spec::default();
let bad_docs = crate::v3_2::external_documentation::ExternalDocumentation {
url: "".into(),
description: None,
extensions: None,
};
let bad_xml = crate::v3_2::xml::XML {
node_type: Some("badtype".into()),
..Default::default()
};
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
StringSchema {
external_docs: Some(bad_docs),
xml: Some(bad_xml),
..Default::default()
}
.validate_with_context(&mut ctx, "s".into());
assert!(
ctx.errors.mentions("externalDocs"),
"missing externalDocs error: {:?}",
ctx.errors
);
assert!(
ctx.errors.mentions("xml"),
"missing xml error: {:?}",
ctx.errors
);
}
#[test]
fn integer_schema_external_docs_and_xml_are_walked() {
let spec = crate::v3_2::spec::Spec::default();
let bad_docs = crate::v3_2::external_documentation::ExternalDocumentation {
url: "".into(),
description: None,
extensions: None,
};
let bad_xml = crate::v3_2::xml::XML {
node_type: Some("badtype".into()),
..Default::default()
};
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
IntegerSchema {
external_docs: Some(bad_docs),
xml: Some(bad_xml),
..Default::default()
}
.validate_with_context(&mut ctx, "s".into());
assert!(ctx.errors.mentions("externalDocs"), "{:?}", ctx.errors);
assert!(ctx.errors.mentions("xml"), "{:?}", ctx.errors);
}
#[test]
fn number_schema_external_docs_and_xml_are_walked() {
let spec = crate::v3_2::spec::Spec::default();
let bad_docs = crate::v3_2::external_documentation::ExternalDocumentation {
url: "".into(),
description: None,
extensions: None,
};
let bad_xml = crate::v3_2::xml::XML {
node_type: Some("badtype".into()),
..Default::default()
};
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
NumberSchema {
external_docs: Some(bad_docs),
xml: Some(bad_xml),
..Default::default()
}
.validate_with_context(&mut ctx, "s".into());
assert!(ctx.errors.mentions("externalDocs"), "{:?}", ctx.errors);
assert!(ctx.errors.mentions("xml"), "{:?}", ctx.errors);
}
#[test]
fn boolean_schema_xml_is_walked() {
let spec = crate::v3_2::spec::Spec::default();
let bad_xml = crate::v3_2::xml::XML {
node_type: Some("badtype".into()),
..Default::default()
};
let s = Schema::Single(Box::new(SingleSchema::Boolean(BooleanSchema {
xml: Some(bad_xml),
..Default::default()
})));
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "s".into());
assert!(ctx.errors.mentions("xml"), "{:?}", ctx.errors);
}
#[test]
fn array_schema_external_docs_and_xml_are_walked() {
let spec = crate::v3_2::spec::Spec::default();
let bad_docs = crate::v3_2::external_documentation::ExternalDocumentation {
url: "".into(),
description: None,
extensions: None,
};
let bad_xml = crate::v3_2::xml::XML {
node_type: Some("badtype".into()),
..Default::default()
};
let s = Schema::Single(Box::new(SingleSchema::Array(ArraySchema {
external_docs: Some(bad_docs),
xml: Some(bad_xml),
..Default::default()
})));
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "s".into());
assert!(ctx.errors.mentions("externalDocs"), "{:?}", ctx.errors);
assert!(ctx.errors.mentions("xml"), "{:?}", ctx.errors);
}
#[test]
fn object_schema_external_docs_and_xml_are_walked() {
let spec = crate::v3_2::spec::Spec::default();
let bad_docs = crate::v3_2::external_documentation::ExternalDocumentation {
url: "".into(),
description: None,
extensions: None,
};
let bad_xml = crate::v3_2::xml::XML {
node_type: Some("badtype".into()),
..Default::default()
};
let s = Schema::Single(Box::new(SingleSchema::Object(ObjectSchema {
external_docs: Some(bad_docs),
xml: Some(bad_xml),
..Default::default()
})));
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "s".into());
assert!(ctx.errors.mentions("externalDocs"), "{:?}", ctx.errors);
assert!(ctx.errors.mentions("xml"), "{:?}", ctx.errors);
}
#[test]
fn object_schema_pattern_properties_are_walked() {
let spec = crate::v3_2::spec::Spec::default();
let bad_docs = crate::v3_2::external_documentation::ExternalDocumentation {
url: "".into(),
description: None,
extensions: None,
};
let s = Schema::Single(Box::new(SingleSchema::Object(ObjectSchema {
pattern_properties: Some(BTreeMap::from([(
"^S_".to_owned(),
RefOr::new_item(Schema::Single(Box::new(SingleSchema::String(
StringSchema {
external_docs: Some(bad_docs),
..Default::default()
},
)))),
)])),
..Default::default()
})));
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "s".into());
assert!(
ctx.errors.mentions("pattern_properties"),
"error should mention pattern_properties path: {:?}",
ctx.errors
);
}
#[test]
fn object_schema_unevaluated_properties_bool_is_no_op() {
let spec = crate::v3_2::spec::Spec::default();
let s = Schema::Single(Box::new(SingleSchema::Object(ObjectSchema {
unevaluated_properties: Some(BoolOr::Bool(false)),
..Default::default()
})));
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "s".into());
assert!(
ctx.errors.is_empty(),
"Bool(false) unevaluatedProperties should produce no errors: {:?}",
ctx.errors
);
}
#[test]
fn object_schema_unevaluated_properties_item_is_walked() {
let spec = crate::v3_2::spec::Spec::default();
let bad_docs = crate::v3_2::external_documentation::ExternalDocumentation {
url: "".into(),
description: None,
extensions: None,
};
let s = Schema::Single(Box::new(SingleSchema::Object(ObjectSchema {
unevaluated_properties: Some(BoolOr::Item(RefOr::new_item(Schema::Single(Box::new(
SingleSchema::String(StringSchema {
external_docs: Some(bad_docs),
..Default::default()
}),
))))),
..Default::default()
})));
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "s".into());
assert!(
ctx.errors.mentions("unevaluatedProperties"),
"error should mention unevaluatedProperties: {:?}",
ctx.errors
);
}
#[test]
fn object_schema_property_names_is_walked() {
let spec = crate::v3_2::spec::Spec::default();
let bad_docs = crate::v3_2::external_documentation::ExternalDocumentation {
url: "".into(),
description: None,
extensions: None,
};
let s = Schema::Single(Box::new(SingleSchema::Object(ObjectSchema {
property_names: Some(RefOr::new_item(Schema::Single(Box::new(
SingleSchema::String(StringSchema {
external_docs: Some(bad_docs),
..Default::default()
}),
)))),
..Default::default()
})));
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "s".into());
assert!(
ctx.errors.mentions("propertyNames"),
"error should mention propertyNames: {:?}",
ctx.errors
);
}
#[test]
fn null_schema_xml_is_walked() {
let spec = crate::v3_2::spec::Spec::default();
let bad_xml = crate::v3_2::xml::XML {
node_type: Some("badtype".into()),
..Default::default()
};
let s = Schema::Single(Box::new(SingleSchema::Null(NullSchema {
xml: Some(bad_xml),
..Default::default()
})));
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
s.validate_with_context(&mut ctx, "s".into());
assert!(ctx.errors.mentions("xml"), "{:?}", ctx.errors);
}
#[test]
fn multi_schema_invalid_type_reported() {
let spec = crate::v3_2::spec::Spec::default();
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
let m = MultiSchema {
schema_types: vec![
SchemaType::String,
SchemaType::Custom("badtype".to_string()),
SchemaType::String,
],
..Default::default()
};
Schema::Multi(Box::new(m)).validate_with_context(&mut ctx, "s".into());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("not supported") && e.contains("badtype")),
"expected 'not supported' error for unknown type: {:?}",
ctx.errors
);
assert!(
ctx.errors
.iter()
.any(|e| e.contains("not unique") && e.contains("string")),
"expected 'not unique' error for duplicate type: {:?}",
ctx.errors
);
}
#[test]
fn multi_schema_external_docs_is_walked() {
let spec = crate::v3_2::spec::Spec::default();
let bad_docs = crate::v3_2::external_documentation::ExternalDocumentation {
url: "".into(),
description: None,
extensions: None,
};
let m = MultiSchema {
schema_types: vec![SchemaType::String],
external_docs: Some(bad_docs),
..Default::default()
};
let mut ctx = crate::validation::Context::new(&spec, crate::validation::Options::new());
Schema::Multi(Box::new(m)).validate_with_context(&mut ctx, "s".into());
assert!(
ctx.errors.mentions("externalDocs"),
"externalDocs should be walked on MultiSchema: {:?}",
ctx.errors
);
}
#[test]
fn extensions_deserialize_duplicate_key_keeps_last() {
let raw = r#"{"type":"object","x-foo":1,"x-foo":2}"#;
let schema: Schema = serde_json::from_str(raw).unwrap();
let json = serde_json::to_value(&schema).unwrap();
assert_eq!(json["x-foo"], serde_json::json!(2));
}
#[test]
fn extensions_serialize_none_branch() {
let s = Schema::Single(Box::new(SingleSchema::String(StringSchema {
title: Some("no-ext".into()),
extensions: None,
..Default::default()
})));
let v = serde_json::to_value(&s).unwrap();
assert!(
!v.as_object().unwrap().keys().any(|k| k.starts_with("x-")),
"no x- keys expected when extensions is None: {v}"
);
}
#[test]
fn schema_deserialize_rejects_non_object_non_boolean() {
let err = serde_json::from_value::<Schema>(serde_json::json!("a string"))
.expect_err("string should not parse as Schema");
assert!(
err.to_string().contains("a JSON object or boolean"),
"expected 'JSON object or boolean' error: {err}"
);
let err2 = serde_json::from_value::<Schema>(serde_json::json!(42))
.expect_err("integer should not parse as Schema");
assert!(
err2.to_string().contains("a JSON object or boolean"),
"expected 'JSON object or boolean' error: {err2}"
);
}
#[test]
fn single_schema_deserialize_directly() {
let s: SingleSchema =
serde_json::from_value(serde_json::json!({"type": "string", "title": "T"}))
.expect("must parse as SingleSchema");
match s {
SingleSchema::String(st) => {
assert_eq!(st.title.as_deref(), Some("T"));
}
other => panic!("expected String, got {other:?}"),
}
let s2: SingleSchema = serde_json::from_value(serde_json::json!({"type": "integer"}))
.expect("must parse as SingleSchema integer");
assert!(matches!(s2, SingleSchema::Integer(_)));
let s3: SingleSchema = serde_json::from_value(serde_json::json!({"type": "array"}))
.expect("must parse as SingleSchema array");
assert!(matches!(s3, SingleSchema::Array(_)));
}
#[test]
fn schema_extensions_visitor_expecting_on_non_map() {
let mut de = serde_json::Deserializer::from_str(r#""not-a-map""#);
let err = super::extensions::deserialize(&mut de).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("map") || msg.contains("expected") || msg.contains("extensions"),
"expected a type-mismatch serde error, got: {msg}"
);
}
#[test]
fn schema_extensions_duplicate_key_errors() {
let json_bytes = br#"{"x-foo": 1, "x-foo": 2}"#;
let mut de = serde_json::Deserializer::from_slice(json_bytes);
let result = super::extensions::deserialize(&mut de);
let _ = result;
}
#[test]
fn schema_extensions_serialize_none_branch() {
let none: Option<std::collections::BTreeMap<String, serde_json::Value>> = None;
let ser = serde_json::value::Serializer;
let result = super::extensions::serialize(&none, ser);
let val = result.unwrap();
assert_eq!(val, serde_json::Value::Null);
}
}