@@ -764,6 +764,65 @@ def test_nullable_schema_combos(
764764 validator .validate ({"testfield" : None })
765765 assert False
766766
767+ def test_ref_ignores_sibling_keywords (self , validator_class ):
768+ schema = {
769+ "$ref" : "#/components/schemas/ItemTarget" ,
770+ "required" : ["name" ],
771+ "components" : {
772+ "schemas" : {
773+ "PlainId" : {
774+ "type" : "integer" ,
775+ },
776+ "NullableName" : {
777+ "type" : "string" ,
778+ "nullable" : True ,
779+ "minLength" : 2 ,
780+ },
781+ "ItemTarget" : {
782+ "type" : "object" ,
783+ "required" : ["id" ],
784+ "properties" : {
785+ "id" : {
786+ "$ref" : "#/components/schemas/PlainId" ,
787+ "type" : "integer" ,
788+ "nullable" : True ,
789+ },
790+ "name" : {
791+ "$ref" : "#/components/schemas/NullableName" ,
792+ "type" : "string" ,
793+ "nullable" : False ,
794+ "minLength" : 10 ,
795+ },
796+ "code" : {
797+ "$ref" : "#/components/schemas/NullableName" ,
798+ "type" : "integer" ,
799+ },
800+ },
801+ },
802+ },
803+ },
804+ }
805+ validator = validator_class (
806+ schema ,
807+ format_checker = oas30_format_checker ,
808+ )
809+
810+ assert validator .validate ({"id" : 1 }) is None
811+ assert (
812+ validator .validate ({"id" : 1 , "name" : None , "code" : "abc" }) is None
813+ )
814+
815+ with pytest .raises (
816+ ValidationError , match = "'id' is a required property"
817+ ):
818+ validator .validate ({"name" : "valid_name" })
819+
820+ with pytest .raises (ValidationError , match = "None for not nullable" ):
821+ validator .validate ({"id" : None })
822+
823+ with pytest .raises (ValidationError , match = "is too short" ):
824+ validator .validate ({"id" : 1 , "name" : "a" })
825+
767826
768827class TestOAS30ReadWriteValidatorValidate :
769828 def test_read_only (self ):
@@ -886,6 +945,90 @@ def test_write_only_false(self):
886945 )
887946 assert validator .validate ({"some_prop" : "hello" }) is None
888947
948+ def test_ref_ignores_sibling_read_write_keywords (self ):
949+ schema = {
950+ "type" : "object" ,
951+ "required" : ["id" , "secret" , "token" ],
952+ "properties" : {
953+ "id" : {
954+ "$ref" : "#/components/schemas/PlainInt" ,
955+ "readOnly" : True ,
956+ },
957+ "secret" : {
958+ "$ref" : "#/components/schemas/PlainString" ,
959+ "writeOnly" : True ,
960+ },
961+ "token" : {
962+ "$ref" : "#/components/schemas/ReadOnlyString" ,
963+ "readOnly" : False ,
964+ },
965+ },
966+ "components" : {
967+ "schemas" : {
968+ "PlainInt" : {"type" : "integer" },
969+ "PlainString" : {"type" : "string" },
970+ "ReadOnlyString" : {"type" : "string" , "readOnly" : True },
971+ },
972+ },
973+ }
974+
975+ write_validator = OAS30WriteValidator (
976+ schema ,
977+ format_checker = oas30_format_checker ,
978+ )
979+ read_validator = OAS30ReadValidator (
980+ schema ,
981+ format_checker = oas30_format_checker ,
982+ )
983+ default_validator = OAS30Validator (
984+ schema ,
985+ format_checker = oas30_format_checker ,
986+ )
987+
988+ # Sibling readOnly: True on `id` and writeOnly: True on `secret`
989+ # are ignored
990+ assert (
991+ read_validator .validate ({"id" : 1 , "secret" : "abc" , "token" : "tok" })
992+ is None
993+ )
994+ assert (
995+ default_validator .validate (
996+ {"id" : 1 , "secret" : "abc" , "token" : "tok" }
997+ )
998+ is None
999+ )
1000+
1001+ # Target ReadOnlyString still enforces readOnly: True on write
1002+ with pytest .raises (
1003+ ValidationError ,
1004+ match = "Tried to write read-only property with tok" ,
1005+ ):
1006+ write_validator .validate (
1007+ {"id" : 1 , "secret" : "abc" , "token" : "tok" }
1008+ )
1009+
1010+ # Sibling readOnly: True on `id` does not exempt `id` from
1011+ # write_required or default required
1012+ with pytest .raises (
1013+ ValidationError , match = "'id' is a required property"
1014+ ):
1015+ write_validator .validate ({"secret" : "abc" })
1016+ with pytest .raises (
1017+ ValidationError , match = "'id' is a required property"
1018+ ):
1019+ default_validator .validate ({"secret" : "abc" , "token" : "tok" })
1020+
1021+ # Sibling writeOnly: True on `secret` does not exempt `secret` from
1022+ # read_required or default required
1023+ with pytest .raises (
1024+ ValidationError , match = "'secret' is a required property"
1025+ ):
1026+ read_validator .validate ({"id" : 1 , "token" : "tok" })
1027+ with pytest .raises (
1028+ ValidationError , match = "'secret' is a required property"
1029+ ):
1030+ default_validator .validate ({"id" : 1 , "token" : "tok" })
1031+
8891032
8901033class TestOAS31ValidatorFormatChecker :
8911034 @pytest .fixture
@@ -1047,6 +1190,40 @@ def test_schema_ref(self, validator_class, format_checker):
10471190 error = "'id' is a required property"
10481191 assert error in str (excinfo .value )
10491192
1193+ def test_schema_ref_applies_sibling_keywords (
1194+ self , validator_class , format_checker
1195+ ):
1196+ schema = {
1197+ "$ref" : "#/$defs/Pet" ,
1198+ "required" : ["name" ],
1199+ "$defs" : {
1200+ "Pet" : {
1201+ "type" : "object" ,
1202+ "required" : ["id" ],
1203+ "properties" : {
1204+ "id" : {"type" : "integer" , "format" : "int64" },
1205+ "name" : {"type" : "string" },
1206+ },
1207+ }
1208+ },
1209+ }
1210+ validator = validator_class (
1211+ schema ,
1212+ format_checker = format_checker ,
1213+ )
1214+
1215+ assert validator .validate ({"id" : 1 , "name" : "John" }) is None
1216+
1217+ with pytest .raises (
1218+ ValidationError , match = "'name' is a required property"
1219+ ):
1220+ validator .validate ({"id" : 1 })
1221+
1222+ with pytest .raises (
1223+ ValidationError , match = "'id' is a required property"
1224+ ):
1225+ validator .validate ({"name" : "John" })
1226+
10501227 @pytest .mark .parametrize (
10511228 "value" ,
10521229 [
@@ -1330,6 +1507,32 @@ def test_strict_binary_format_rejects_str(self):
13301507 with pytest .raises (ValidationError , match = "is not a 'binary'" ):
13311508 validator .validate ("not base64" )
13321509
1510+ def test_strict_validator_ref_ignores_sibling_keywords (self ):
1511+ schema = {
1512+ "$ref" : "#/components/schemas/NullableText" ,
1513+ "type" : "string" ,
1514+ "nullable" : False ,
1515+ "minLength" : 10 ,
1516+ "components" : {
1517+ "schemas" : {
1518+ "NullableText" : {
1519+ "type" : "string" ,
1520+ "nullable" : True ,
1521+ "minLength" : 2 ,
1522+ }
1523+ }
1524+ },
1525+ }
1526+ validator = OAS30StrictValidator (
1527+ schema , format_checker = oas30_strict_format_checker
1528+ )
1529+
1530+ assert validator .validate ("abc" ) is None
1531+ assert validator .validate (None ) is None
1532+
1533+ with pytest .raises (ValidationError , match = "is too short" ):
1534+ validator .validate ("a" )
1535+
13331536
13341537class TestValidatorForDiscovery :
13351538 def test_oas31_base_dialect_resolves_to_oas31_validator (self ):
0 commit comments