@@ -106,8 +106,9 @@ assert_eq!(
106106```
107107
108108*/
109- use crate :: Schema ;
110109use crate :: _alloc_prelude:: * ;
110+ use crate :: { consts:: meta_schemas, Schema } ;
111+ use alloc:: borrow:: Cow ;
111112use alloc:: collections:: BTreeSet ;
112113use serde_json:: { json, Map , Value } ;
113114
@@ -271,6 +272,7 @@ where
271272}
272273
273274/// Replaces boolean JSON Schemas with equivalent object schemas.
275+ ///
274276/// This also applies to subschemas.
275277///
276278/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support booleans as
@@ -306,7 +308,9 @@ impl Transform for ReplaceBoolSchemas {
306308}
307309
308310/// Restructures JSON Schema objects so that the `$ref` property will never appear alongside any
309- /// other properties. This also applies to subschemas.
311+ /// other properties.
312+ ///
313+ /// This also applies to subschemas.
310314///
311315/// This is useful for versions of JSON Schema (e.g. Draft 7) that do not support other properties
312316/// alongside `$ref`.
@@ -332,7 +336,9 @@ impl Transform for RemoveRefSiblings {
332336}
333337
334338/// Removes the `examples` schema property and (if present) set its first value as the `example`
335- /// property. This also applies to subschemas.
339+ /// property.
340+ ///
341+ /// This also applies to subschemas.
336342///
337343/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support the `examples`
338344/// property.
@@ -353,6 +359,7 @@ impl Transform for SetSingleExample {
353359}
354360
355361/// Replaces the `const` schema property with a single-valued `enum` property.
362+ ///
356363/// This also applies to subschemas.
357364///
358365/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that do not support the `const`
@@ -372,6 +379,7 @@ impl Transform for ReplaceConstValue {
372379}
373380
374381/// Rename the `prefixItems` schema property to `items`.
382+ ///
375383/// This also applies to subschemas.
376384///
377385/// If the schema contains both `prefixItems` and `items`, then this additionally renames `items` to
@@ -398,6 +406,7 @@ impl Transform for ReplacePrefixItems {
398406}
399407
400408/// Adds a `"nullable": true` property to schemas that allow `null` types.
409+ ///
401410/// This also applies to subschemas.
402411///
403412/// This is useful for dialects of JSON Schema (e.g. OpenAPI 3.0) that use `nullable` instead of
@@ -461,6 +470,7 @@ impl Transform for AddNullable {
461470
462471/// Replaces the `unevaluatedProperties` schema property with the `additionalProperties` property,
463472/// adding properties from a schema's subschemas to its `properties` where necessary.
473+ ///
464474/// This also applies to subschemas.
465475///
466476/// This is useful for versions of JSON Schema (e.g. Draft 7) that do not support the
@@ -519,3 +529,232 @@ impl Transform for GatherPropertyNames {
519529 transform_immediate_subschemas ( self , schema) ;
520530 }
521531}
532+
533+ /// Removes any `format` values that are not defined by the JSON Schema standard or explicitly
534+ /// allowed by a custom list.
535+ ///
536+ /// This also applies to subschemas.
537+ ///
538+ /// By default, this will infer the version of JSON Schema from the schema's `$schema` property,
539+ /// and no additional formats will be allowed (even when the JSON schema allows nonstandard
540+ /// formats).
541+ ///
542+ /// # Example
543+ /// ```
544+ /// use schemars::json_schema;
545+ /// use schemars::transform::{RestrictFormats, Transform};
546+ ///
547+ /// let mut schema = schemars::json_schema!({
548+ /// "$schema": "https://json-schema.org/draft/2020-12/schema",
549+ /// "anyOf": [
550+ /// {
551+ /// "type": "string",
552+ /// "format": "uuid"
553+ /// },
554+ /// {
555+ /// "$schema": "http://json-schema.org/draft-07/schema#",
556+ /// "type": "string",
557+ /// "format": "uuid"
558+ /// },
559+ /// {
560+ /// "type": "string",
561+ /// "format": "allowed-custom-format"
562+ /// },
563+ /// {
564+ /// "type": "string",
565+ /// "format": "forbidden-custom-format"
566+ /// }
567+ /// ]
568+ /// });
569+ ///
570+ /// let mut transform = RestrictFormats::default();
571+ /// transform.allowed_formats.insert("allowed-custom-format".into());
572+ /// transform.transform(&mut schema);
573+ ///
574+ /// assert_eq!(
575+ /// schema,
576+ /// json_schema!({
577+ /// "$schema": "https://json-schema.org/draft/2020-12/schema",
578+ /// "anyOf": [
579+ /// {
580+ /// // "uuid" format is defined in draft 2020-12.
581+ /// "type": "string",
582+ /// "format": "uuid"
583+ /// },
584+ /// {
585+ /// // "uuid" format is not defined in draft-07, so is removed from this subschema.
586+ /// "$schema": "http://json-schema.org/draft-07/schema#",
587+ /// "type": "string"
588+ /// },
589+ /// {
590+ /// // "allowed-custom-format" format was present in `allowed_formats`...
591+ /// "type": "string",
592+ /// "format": "allowed-custom-format"
593+ /// },
594+ /// {
595+ /// // ...but "forbidden-custom-format" format was not, so is also removed.
596+ /// "type": "string"
597+ /// }
598+ /// ]
599+ /// })
600+ /// );
601+ /// ```
602+ #[ derive( Debug , Clone ) ]
603+ #[ non_exhaustive]
604+ pub struct RestrictFormats {
605+ /// Whether to read the schema's `$schema` property to determine which version of JSON Schema
606+ /// is being used, and allow only formats defined in that standard. If this is `true` but the
607+ /// JSON Schema version can't be determined because `$schema` is missing or unknown, then no
608+ /// `format` values will be removed.
609+ ///
610+ /// If this is set to `false`, then only the formats explicitly included in
611+ /// [`allowed_formats`](Self::allowed_formats) will be allowed.
612+ ///
613+ /// By default, this is `true`.
614+ pub infer_from_meta_schema : bool ,
615+ /// Values of the `format` property in schemas that will always be allowed, regardless of the
616+ /// inferred version of JSON Schema.
617+ pub allowed_formats : BTreeSet < Cow < ' static , str > > ,
618+ }
619+
620+ impl Default for RestrictFormats {
621+ fn default ( ) -> Self {
622+ Self {
623+ infer_from_meta_schema : true ,
624+ allowed_formats : BTreeSet :: new ( ) ,
625+ }
626+ }
627+ }
628+
629+ impl Transform for RestrictFormats {
630+ fn transform ( & mut self , schema : & mut Schema ) {
631+ let mut implementation = RestrictFormatsImpl {
632+ infer_from_meta_schema : self . infer_from_meta_schema ,
633+ inferred_formats : None ,
634+ allowed_formats : & self . allowed_formats ,
635+ } ;
636+
637+ implementation. transform ( schema) ;
638+ }
639+ }
640+
641+ static DEFINED_FORMATS : & [ & str ] = & [
642+ // `duration` and `uuid` are defined only in draft 2019-09+
643+ "duration" ,
644+ "uuid" ,
645+ // The rest are also defined in draft-07:
646+ "date-time" ,
647+ "date" ,
648+ "time" ,
649+ "email" ,
650+ "idn-email" ,
651+ "hostname" ,
652+ "idn-hostname" ,
653+ "ipv4" ,
654+ "ipv6" ,
655+ "uri" ,
656+ "uri-reference" ,
657+ "iri" ,
658+ "iri-reference" ,
659+ "uri-template" ,
660+ "json-pointer" ,
661+ "relative-json-pointer" ,
662+ "regex" ,
663+ ] ;
664+
665+ struct RestrictFormatsImpl < ' a > {
666+ infer_from_meta_schema : bool ,
667+ inferred_formats : Option < & ' static [ & ' static str ] > ,
668+ allowed_formats : & ' a BTreeSet < Cow < ' static , str > > ,
669+ }
670+
671+ impl Transform for RestrictFormatsImpl < ' _ > {
672+ fn transform ( & mut self , schema : & mut Schema ) {
673+ let Some ( obj) = schema. as_object_mut ( ) else {
674+ return ;
675+ } ;
676+
677+ let previous_inferred_formats = self . inferred_formats ;
678+
679+ if self . infer_from_meta_schema && obj. contains_key ( "$schema" ) {
680+ self . inferred_formats = match obj
681+ . get ( "$schema" )
682+ . and_then ( Value :: as_str)
683+ . unwrap_or_default ( )
684+ {
685+ meta_schemas:: DRAFT07 => Some ( & DEFINED_FORMATS [ 2 ..] ) ,
686+ meta_schemas:: DRAFT2019_09 | meta_schemas:: DRAFT2020_12 => Some ( DEFINED_FORMATS ) ,
687+ _ => {
688+ // we can't handle an unrecognised meta-schema
689+ return ;
690+ }
691+ } ;
692+ }
693+
694+ if let Some ( format) = obj. get ( "format" ) . and_then ( Value :: as_str) {
695+ if !self . allowed_formats . contains ( format)
696+ && !self
697+ . inferred_formats
698+ . is_some_and ( |formats| formats. contains ( & format) )
699+ {
700+ obj. remove ( "format" ) ;
701+ }
702+ }
703+
704+ transform_subschemas ( self , schema) ;
705+
706+ self . inferred_formats = previous_inferred_formats;
707+ }
708+ }
709+
710+ #[ cfg( test) ]
711+ mod tests {
712+ use super :: * ;
713+ use pretty_assertions:: assert_eq;
714+
715+ #[ test]
716+ fn restrict_formats ( ) {
717+ let mut schema = json_schema ! ( {
718+ "$schema" : meta_schemas:: DRAFT2020_12 ,
719+ "anyOf" : [
720+ { "format" : "uuid" } ,
721+ { "$schema" : meta_schemas:: DRAFT07 , "format" : "uuid" } ,
722+ { "$schema" : "http://unknown" , "format" : "uuid" } ,
723+ { "format" : "date" } ,
724+ { "$schema" : meta_schemas:: DRAFT07 , "format" : "date" } ,
725+ { "$schema" : "http://unknown" , "format" : "date" } ,
726+ { "format" : "custom1" } ,
727+ { "$schema" : meta_schemas:: DRAFT07 , "format" : "custom1" } ,
728+ { "$schema" : "http://unknown" , "format" : "custom1" } ,
729+ { "format" : "custom2" } ,
730+ { "$schema" : meta_schemas:: DRAFT07 , "format" : "custom2" } ,
731+ { "$schema" : "http://unknown" , "format" : "custom2" } ,
732+ ]
733+ } ) ;
734+
735+ let mut transform = RestrictFormats :: default ( ) ;
736+ transform. allowed_formats . insert ( "custom1" . into ( ) ) ;
737+ transform. transform ( & mut schema) ;
738+
739+ assert_eq ! (
740+ schema,
741+ json_schema!( {
742+ "$schema" : meta_schemas:: DRAFT2020_12 ,
743+ "anyOf" : [
744+ { "format" : "uuid" } ,
745+ { "$schema" : meta_schemas:: DRAFT07 } ,
746+ { "$schema" : "http://unknown" , "format" : "uuid" } ,
747+ { "format" : "date" } ,
748+ { "$schema" : meta_schemas:: DRAFT07 , "format" : "date" } ,
749+ { "$schema" : "http://unknown" , "format" : "date" } ,
750+ { "format" : "custom1" } ,
751+ { "$schema" : meta_schemas:: DRAFT07 , "format" : "custom1" } ,
752+ { "$schema" : "http://unknown" , "format" : "custom1" } ,
753+ { } ,
754+ { "$schema" : meta_schemas:: DRAFT07 } ,
755+ { "$schema" : "http://unknown" , "format" : "custom2" } ,
756+ ]
757+ } )
758+ ) ;
759+ }
760+ }
0 commit comments