@@ -28,6 +28,7 @@ def __init__(
2828 description : str = "" ,
2929 default : typing .Any = NO_DEFAULT ,
3030 allow_null : bool = False ,
31+ read_only : bool = False
3132 ):
3233 assert isinstance (title , str )
3334 assert isinstance (description , str )
@@ -41,6 +42,7 @@ def __init__(
4142 self .title = title
4243 self .description = description
4344 self .allow_null = allow_null
45+ self .read_only = read_only
4446
4547 # We need this global counter to determine what order fields have
4648 # been declared in when used with `Schema`.
@@ -680,6 +682,85 @@ def serialize(self, obj: typing.Any) -> typing.Any:
680682 return [self .items .serialize (value ) for value in obj ]
681683
682684
685+ class Scheme (Field ):
686+ errors = {
687+ "type" : "Must be an object." ,
688+ "null" : "May not be null." ,
689+ "invalid_key" : "All object keys must be strings." ,
690+ "required" : "This field is required." ,
691+ }
692+
693+ def __init__ (
694+ self ,
695+ fields : typing .Dict [str , Field ],
696+ ** kwargs : typing .Any ,
697+ ) -> None :
698+ super ().__init__ (** kwargs )
699+ self .fields = fields
700+ self .required = [key for key , field in fields .items () if not (field .read_only or field .has_default ())]
701+
702+ def validate (self , value : typing .Any ) -> typing .Any :
703+ if value is None and self .allow_null :
704+ return None
705+ elif value is None :
706+ raise self .validation_error ("null" )
707+ elif not isinstance (value , (dict , typing .Mapping )):
708+ raise self .validation_error ("type" )
709+
710+ validated = {}
711+ error_messages = []
712+
713+ # Ensure all property keys are strings.
714+ for key in value .keys ():
715+ if not isinstance (key , str ):
716+ text = self .get_error_text ("invalid_key" )
717+ message = Message (text = text , code = "invalid_key" , index = [key ])
718+ error_messages .append (message )
719+
720+ # Required properties
721+ for key in self .required :
722+ if key not in value :
723+ text = self .get_error_text ("required" )
724+ message = Message (text = text , code = "required" , index = [key ])
725+ error_messages .append (message )
726+
727+ # Properties
728+ for key , child_schema in self .fields .items ():
729+ if child_schema .read_only :
730+ continue
731+
732+ if key not in value :
733+ if child_schema .has_default ():
734+ validated [key ] = child_schema .get_default_value ()
735+ continue
736+ item = value [key ]
737+ child_value , error = child_schema .validate_or_error (item )
738+ if not error :
739+ validated [key ] = child_value
740+ else :
741+ error_messages += error .messages (add_prefix = key )
742+
743+ if error_messages :
744+ raise ValidationError (messages = error_messages )
745+
746+ return validated
747+
748+ def serialize (self , obj : typing .Any ) -> typing .Any :
749+ if obj is None :
750+ return None
751+
752+ is_mapping = isinstance (obj , dict )
753+
754+ ret = {}
755+ for key , field in self .fields .items ():
756+ try :
757+ value = obj [key ] if is_mapping else getattr (obj , key )
758+ except (KeyError , AttributeError ):
759+ continue
760+ ret [key ] = field .serialize (value )
761+ return ret
762+
763+
683764class Text (String ):
684765 def __init__ (self , ** kwargs : typing .Any ) -> None :
685766 super ().__init__ (format = "text" , ** kwargs )
0 commit comments