Repository: jcrist/msgspec
Pull request #213 (2022-11-28 04:22:43)

Support `forbid_unknown_fields` in json-schema generation
When `forbid_unknown_fields` was added to the `Struct` configuration, I forgot to update the `msgspec.json.schema` functions to augment the generated schemas with this configuration. This PR fixes that.


Patch:
diff --git a/msgspec/_core.c b/msgspec/_core.c
index b44f6e3..6ea9996 100644
--- a/msgspec/_core.c
+++ b/msgspec/_core.c
@@ -479,7 +479,6 @@ typedef struct {
     PyObject *str__value2member_map_;
     PyObject *str___msgspec_cache__;
     PyObject *str__value_;
-    PyObject *str_value;
     PyObject *str_type;
     PyObject *str_enc_hook;
     PyObject *str_dec_hook;
@@ -13796,7 +13795,6 @@ msgspec_clear(PyObject *m)
     Py_CLEAR(st->str__value2member_map_);
     Py_CLEAR(st->str___msgspec_cache__);
     Py_CLEAR(st->str__value_);
-    Py_CLEAR(st->str_value);
     Py_CLEAR(st->str_type);
     Py_CLEAR(st->str_enc_hook);
     Py_CLEAR(st->str_dec_hook);
@@ -14134,7 +14132,6 @@ PyInit__core(void)
     CACHED_STRING(str__value2member_map_, "_value2member_map_");
     CACHED_STRING(str___msgspec_cache__, "__msgspec_cache__");
     CACHED_STRING(str__value_, "_value_");
-    CACHED_STRING(str_value, "value");
     CACHED_STRING(str_type, "type");
     CACHED_STRING(str_enc_hook, "enc_hook");
     CACHED_STRING(str_dec_hook, "dec_hook");
diff --git a/msgspec/_utils.py b/msgspec/_utils.py
index 82ae00a..82bc47e 100644
--- a/msgspec/_utils.py
+++ b/msgspec/_utils.py
@@ -477,10 +477,14 @@ class SchemaBuilder:
                 schema["type"] = "array"
                 schema["prefixItems"] = fields
                 schema["minItems"] = len(required)
+                if t.forbid_unknown_fields:
+                    schema["maxItems"] = len(t.__struct_fields__)
             else:
                 schema["type"] = "object"
                 schema["properties"] = dict(zip(names, fields))
                 schema["required"] = required
+                if t.forbid_unknown_fields:
+                    schema["additionalProperties"] = False
         elif is_typeddict(t):
             schema.setdefault("title", t.__name__)
             if has_nondefault_docstring(t):


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
index bf8fb31..0ac3da7 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -222,7 +222,7 @@ def test_str_literal():
 
 
 def test_struct_object():
-    class Point(msgspec.Struct):
+    class Point(msgspec.Struct, forbid_unknown_fields=True):
         x: int
         y: int
 
@@ -265,13 +265,17 @@ def test_struct_object():
                     "y": {"type": "integer"},
                 },
                 "required": ["x", "y"],
+                "additionalProperties": False,
             },
         },
     }
 
 
-def test_struct_array_like():
-    class Example(msgspec.Struct, array_like=True):
+@pytest.mark.parametrize("forbid_unknown_fields", [False, True])
+def test_struct_array_like(forbid_unknown_fields):
+    class Example(
+        msgspec.Struct, array_like=True, forbid_unknown_fields=forbid_unknown_fields
+    ):
         """An example docstring"""
 
         a: int
@@ -279,7 +283,7 @@ def test_struct_array_like():
         c: List[int] = []
         d: Dict[str, int] = {}
 
-    assert msgspec.json.schema(Example) == {
+    sol = {
         "$ref": "#/$defs/Example",
         "$defs": {
             "Example": {
@@ -300,6 +304,9 @@ def test_struct_array_like():
             }
         },
     }
+    if forbid_unknown_fields:
+        sol["$defs"]["Example"]["maxItems"] = 4
+    assert msgspec.json.schema(Example) == sol
 
 
 def test_struct_no_fields():