diff --git a/README.md b/README.md
index 3a61ddad..e4b5ae38 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,7 @@ python-benedict is a dict subclass with **keylist/keypath/keyattr** support, **I
- **Keypath** support using **keypath-separator** *(dot syntax by default)*.
- Keypath **list-index** support *(also negative)* using the standard `[n]` suffix.
- Normalized **I/O operations** with most common formats: `base64`, `cli`, `csv`, `html`, `ini`, `json`, `pickle`, `plist`, `query-string`, `toml`, `xls`, `xml`, `yaml`.
+- `NEW` Optional **Pydantic v2 schema** validation and type coercion on all `from_*` / `to_*` I/O methods via the `schema` kwarg *(requires `python-benedict[schema]`).*
- Multiple **I/O operations** backends: `file-system` *(read/write)*, `url` *(read-only)*, `s3` *(read/write)*.
- Many **utility** and **parse methods** to retrieve data as needed *(check the [API](#api) section)*.
- Well **tested**. ;)
@@ -68,6 +69,7 @@ Here the hierarchy of possible installation targets available when running `pip
- `[yaml]`
- `[parse]`
- `[s3]`
+ - `[schema]`
## Usage
@@ -614,6 +616,29 @@ d.unique()
These methods are available for input/output operations.
+All `from_*` and `to_*` methods accept an optional `schema` keyword argument. When a [Pydantic v2](https://docs.pydantic.dev/) model class is passed, the data is validated and type-coerced through the model before being returned (on decode) or serialized (on encode). This requires the `python-benedict[schema]` extra.
+
+```
+pip install "python-benedict[schema]"
+```
+
+```python
+from benedict import benedict
+from pydantic import BaseModel
+
+class User(BaseModel):
+ name: str
+ age: int
+
+# validate and coerce types on decode
+d = benedict.from_json('{"name": "Alice", "age": "30"}', schema=User)
+assert d["age"] == 30 # coerced from str to int
+
+# validate and coerce types on encode
+d = benedict({"name": "Bob", "age": "25"})
+s = d.to_json(schema=User) # age is coerced to int before serialization
+```
+
#### `from_base64`
```python
@@ -666,6 +691,7 @@ d = benedict.from_html(s, **kwargs)
# Accept as first argument: url, filepath or data-string.
# It's possible to pass decoder specific options using kwargs:
# https://docs.python.org/3/library/configparser.html
+# It's possible to pass a Pydantic v2 model class as schema= to validate and coerce data.
# A ValueError is raised in case of failure.
d = benedict.from_ini(s, **kwargs)
```
@@ -677,6 +703,7 @@ d = benedict.from_ini(s, **kwargs)
# Accept as first argument: url, filepath or data-string.
# It's possible to pass decoder specific options using kwargs:
# https://docs.python.org/3/library/json.html
+# It's possible to pass a Pydantic v2 model class as schema= to validate and coerce data.
# A ValueError is raised in case of failure.
d = benedict.from_json(s, **kwargs)
```
@@ -753,6 +780,7 @@ d = benedict.from_xml(s, **kwargs)
# Accept as first argument: url, filepath or data-string.
# It's possible to pass decoder specific options using kwargs:
# https://pyyaml.org/wiki/PyYAMLDocumentation
+# It's possible to pass a Pydantic v2 model class as schema= to validate and coerce data.
# A ValueError is raised in case of failure.
d = benedict.from_yaml(s, **kwargs)
```
@@ -795,6 +823,7 @@ s = d.to_ini(**kwargs)
# Return the dict instance encoded in json format and optionally save it at the specified filepath.
# It's possible to pass encoder specific options using kwargs:
# https://docs.python.org/3/library/json.html
+# It's possible to pass a Pydantic v2 model class as schema= to validate and coerce data before encoding.
# A ValueError is raised in case of failure.
s = d.to_json(**kwargs)
```
diff --git a/benedict/dicts/io/io_util.py b/benedict/dicts/io/io_util.py
index 5ec10a23..3156c7b2 100644
--- a/benedict/dicts/io/io_util.py
+++ b/benedict/dicts/io/io_util.py
@@ -27,7 +27,7 @@
get_format_by_path,
get_serializer_by_format,
)
-from benedict.utils import type_util
+from benedict.utils import schema_util, type_util
def autodetect_format(s: Any) -> str | None:
@@ -61,10 +61,13 @@ def decode(s: Any, format: str, **kwargs: Any) -> Any:
if not serializer:
raise ValueError(f"Invalid format: {format}.")
options = kwargs.copy()
+ schema = options.pop("schema", None)
if format in ["b64", "base64"]:
options.setdefault("subformat", "json")
content = read_content(s, format, options)
data = serializer.decode(content, **options)
+ if schema is not None:
+ data = schema_util.apply_schema(data, schema)
return data
@@ -73,6 +76,9 @@ def encode(d: Any, format: str, filepath: str | None = None, **kwargs: Any) -> A
if not serializer:
raise ValueError(f"Invalid format: {format}.")
options = kwargs.copy()
+ schema = options.pop("schema", None)
+ if schema is not None:
+ d = schema_util.apply_schema(d, schema)
content = serializer.encode(d, **options)
if filepath:
filepath = str(filepath)
diff --git a/benedict/extras.py b/benedict/extras.py
index f69c3eb3..6f09db6b 100644
--- a/benedict/extras.py
+++ b/benedict/extras.py
@@ -5,6 +5,7 @@
"require_html",
"require_parse",
"require_s3",
+ "require_schema",
"require_toml",
"require_xls",
"require_xml",
@@ -33,6 +34,10 @@ def require_s3(*, installed: bool) -> None:
_require_optional_dependencies(target="s3", installed=installed)
+def require_schema(*, installed: bool) -> None:
+ _require_optional_dependencies(target="schema", installed=installed)
+
+
def require_toml(*, installed: bool) -> None:
_require_optional_dependencies(target="toml", installed=installed)
diff --git a/benedict/utils/schema_util.py b/benedict/utils/schema_util.py
new file mode 100644
index 00000000..1bcda513
--- /dev/null
+++ b/benedict/utils/schema_util.py
@@ -0,0 +1,30 @@
+from __future__ import annotations
+
+from typing import Any
+
+try:
+ import pydantic
+
+ pydantic_installed = True
+except ImportError: # pragma: no cover
+ pydantic_installed = False
+
+
+def apply_schema(data: Any, schema: Any) -> Any:
+ """
+ Validate and parse data using a Pydantic model class.
+ Returns the validated data as a plain dict.
+ Raises ExtrasRequireModuleNotFoundError if pydantic is not installed.
+ Raises TypeError if schema is not a pydantic BaseModel subclass.
+ """
+ from benedict.extras import require_schema
+
+ require_schema(installed=pydantic_installed)
+ if isinstance(schema, type) and issubclass(schema, pydantic.BaseModel):
+ schema_cls: type[pydantic.BaseModel] = schema
+ else:
+ raise TypeError(
+ f"schema must be a pydantic BaseModel subclass, got {type(schema)!r}"
+ )
+ instance = schema_cls.model_validate(data)
+ return instance.model_dump()
diff --git a/pyproject.toml b/pyproject.toml
index 7cc48cca..b349144b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -116,7 +116,7 @@ Twitter = "https://twitter.com/fabiocaccamo"
[project.optional-dependencies]
all = [
- "python-benedict[io,parse,s3]",
+ "python-benedict[io,parse,s3,schema]",
]
html = [
"beautifulsoup4 >= 4.12.0, < 5.0.0",
@@ -137,6 +137,9 @@ s3 = [
"boto3 >= 1.24.89, < 2.0.0",
"python-fsutil >= 0.16.1, < 1.0.0",
]
+schema = [
+ "pydantic >= 2.0.0, < 3.0.0",
+]
toml = [
"toml >= 0.10.2, < 1.0.0",
]
diff --git a/requirements.txt b/requirements.txt
index c80163e4..c1643f52 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,6 +6,7 @@ idna >= 3.7
mailchecker == 6.0.20
openpyxl == 3.1.5
phonenumbers == 9.0.27
+pydantic >= 2.0.0, < 3.0.0
python-dateutil == 2.9.0.post0
python-fsutil == 0.16.1
python-slugify == 8.0.4
diff --git a/tests/dicts/io/test_schema.py b/tests/dicts/io/test_schema.py
new file mode 100644
index 00000000..9088639d
--- /dev/null
+++ b/tests/dicts/io/test_schema.py
@@ -0,0 +1,489 @@
+from __future__ import annotations
+
+import base64
+import importlib
+import json
+import pickle
+import plistlib
+import sys
+import unittest
+from unittest.mock import patch
+
+import pydantic
+
+from benedict import benedict
+from benedict.dicts.io import IODict
+from benedict.exceptions import ExtrasRequireModuleNotFoundError
+from benedict.utils import schema_util
+
+
+class UserSchema(pydantic.BaseModel):
+ name: str
+ age: int
+
+
+class XMLWrappedSchema(pydantic.BaseModel):
+ root: UserSchema
+
+
+class schema_util_test_case(unittest.TestCase):
+ def test_apply_schema_valid(self) -> None:
+ data = {"name": "Alice", "age": 30}
+ result = schema_util.apply_schema(data, UserSchema)
+ self.assertEqual(result, {"name": "Alice", "age": 30})
+
+ def test_apply_schema_coerces_types(self) -> None:
+ data = {"name": "Bob", "age": "25"}
+ result = schema_util.apply_schema(data, UserSchema)
+ self.assertEqual(result, {"name": "Bob", "age": 25})
+ self.assertIsInstance(result["age"], int)
+
+ def test_apply_schema_strips_extra_fields(self) -> None:
+ # Fields not declared in the schema must be absent from the result.
+ data = {"name": "Alice", "age": 30, "email": "alice@example.com", "extra": 42}
+ result = schema_util.apply_schema(data, UserSchema)
+ self.assertEqual(result, {"name": "Alice", "age": 30})
+ self.assertNotIn("email", result)
+ self.assertNotIn("extra", result)
+
+ def test_apply_schema_missing_required_field_raises(self) -> None:
+ # A missing required field must raise a ValidationError.
+ data = {"name": "Alice"} # 'age' is required but absent
+ with self.assertRaises(pydantic.ValidationError):
+ schema_util.apply_schema(data, UserSchema)
+
+ def test_apply_schema_invalid_data_raises(self) -> None:
+ data = {"name": "Charlie", "age": "not-a-number"}
+ with self.assertRaises(pydantic.ValidationError):
+ schema_util.apply_schema(data, UserSchema)
+
+ def test_apply_schema_pydantic_not_installed_raises(self) -> None:
+ with patch.object(schema_util, "pydantic_installed", False):
+ with self.assertRaises(ExtrasRequireModuleNotFoundError):
+ schema_util.apply_schema({"name": "X", "age": 1}, UserSchema)
+
+ def test_apply_schema_non_class_raises_type_error(self) -> None:
+ with self.assertRaises(TypeError):
+ schema_util.apply_schema({"name": "Alice", "age": 30}, "not-a-class")
+
+ def test_apply_schema_non_basemodel_class_raises_type_error(self) -> None:
+ class NotAModel:
+ pass
+
+ with self.assertRaises(TypeError):
+ schema_util.apply_schema({"name": "Alice", "age": 30}, NotAModel)
+
+ def test_apply_schema_instance_raises_type_error(self) -> None:
+ instance = UserSchema(name="Alice", age=30)
+ with self.assertRaises(TypeError):
+ schema_util.apply_schema({"name": "Alice", "age": 30}, instance)
+
+ def test_pydantic_not_installed_at_import_time(self) -> None:
+ module_key = "benedict.utils.schema_util"
+ original_module = sys.modules.pop(module_key, None)
+ try:
+ with patch.dict("sys.modules", {"pydantic": None}):
+ reimported = importlib.import_module(module_key)
+ self.assertFalse(reimported.pydantic_installed)
+ finally:
+ if original_module is not None:
+ sys.modules[module_key] = original_module
+
+
+class schema_extras_test_case(unittest.TestCase):
+ def test_require_schema_error_message(self) -> None:
+ from benedict.extras import require_schema
+
+ with self.assertRaises(ExtrasRequireModuleNotFoundError) as ctx:
+ require_schema(installed=False)
+ self.assertIn("schema", str(ctx.exception))
+ self.assertIn("python-benedict[schema]", str(ctx.exception))
+
+
+class io_dict_from_json_schema_test_case(unittest.TestCase):
+ def test_from_json_with_schema_valid(self) -> None:
+ j = '{"name": "Alice", "age": 30}'
+ d = IODict.from_json(j, schema=UserSchema)
+ self.assertEqual(d, {"name": "Alice", "age": 30})
+
+ def test_from_json_with_schema_coerces_types(self) -> None:
+ j = '{"name": "Bob", "age": "25"}'
+ d = IODict.from_json(j, schema=UserSchema)
+ self.assertIsInstance(d["age"], int)
+ self.assertEqual(d["age"], 25)
+
+ def test_from_json_with_schema_invalid_raises(self) -> None:
+ j = '{"name": "Charlie", "age": "not-a-number"}'
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ IODict.from_json(j, schema=UserSchema)
+
+ def test_from_json_with_schema_strips_extra_fields(self) -> None:
+ j = '{"name": "Alice", "age": 30, "email": "alice@example.com"}'
+ d = IODict.from_json(j, schema=UserSchema)
+ self.assertEqual(d, {"name": "Alice", "age": 30})
+ self.assertNotIn("email", d)
+
+ def test_from_json_with_schema_missing_required_field_raises(self) -> None:
+ j = '{"name": "Alice"}' # 'age' required
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ IODict.from_json(j, schema=UserSchema)
+
+ def test_from_json_without_schema(self) -> None:
+ j = '{"name": "Alice", "age": 30}'
+ d = IODict.from_json(j)
+ self.assertEqual(d, {"name": "Alice", "age": 30})
+
+
+class io_dict_from_yaml_schema_test_case(unittest.TestCase):
+ def test_from_yaml_with_schema_valid(self) -> None:
+ y = "name: Alice\nage: 30\n"
+ d = IODict.from_yaml(y, schema=UserSchema)
+ self.assertEqual(d, {"name": "Alice", "age": 30})
+
+ def test_from_yaml_with_schema_strips_extra_fields(self) -> None:
+ y = "name: Alice\nage: 30\nemail: alice@example.com\n"
+ d = IODict.from_yaml(y, schema=UserSchema)
+ self.assertEqual(d, {"name": "Alice", "age": 30})
+ self.assertNotIn("email", d)
+
+ def test_from_yaml_with_schema_missing_required_field_raises(self) -> None:
+ y = "name: Alice\n" # 'age' required
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ IODict.from_yaml(y, schema=UserSchema)
+
+
+class io_dict_from_ini_schema_test_case(unittest.TestCase):
+ def test_from_ini_with_schema_valid(self) -> None:
+ ini = "[DEFAULT]\nname = Alice\nage = 30\n"
+ d = IODict.from_ini(ini, schema=UserSchema)
+ self.assertEqual(d, {"name": "Alice", "age": 30})
+ self.assertIsInstance(d["age"], int)
+
+ def test_from_ini_with_schema_strips_extra_fields(self) -> None:
+ ini = "[DEFAULT]\nname = Alice\nage = 30\nemail = alice@example.com\n"
+ d = IODict.from_ini(ini, schema=UserSchema)
+ self.assertEqual(d, {"name": "Alice", "age": 30})
+ self.assertNotIn("email", d)
+
+ def test_from_ini_with_schema_missing_required_field_raises(self) -> None:
+ ini = "[DEFAULT]\nname = Alice\n" # 'age' required
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ IODict.from_ini(ini, schema=UserSchema)
+
+
+class io_dict_to_json_schema_test_case(unittest.TestCase):
+ def test_to_json_with_schema_valid(self) -> None:
+ d = benedict({"name": "Alice", "age": 30})
+ result = d.to_json(schema=UserSchema)
+ self.assertIn('"name": "Alice"', result)
+ self.assertIn('"age": 30', result)
+
+ def test_to_json_with_schema_coerces_types(self) -> None:
+ d = benedict({"name": "Bob", "age": "25"})
+ result = d.to_json(schema=UserSchema)
+ parsed = json.loads(result)
+ self.assertEqual(parsed["age"], 25)
+ self.assertIsInstance(parsed["age"], int)
+
+ def test_to_json_with_schema_invalid_raises(self) -> None:
+ d = benedict({"name": "Charlie", "age": "not-a-number"})
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ d.to_json(schema=UserSchema)
+
+ def test_to_json_with_schema_strips_extra_fields(self) -> None:
+ d = benedict({"name": "Alice", "age": 30, "email": "alice@example.com"})
+ result = d.to_json(schema=UserSchema)
+ parsed = json.loads(result)
+ self.assertEqual(parsed, {"name": "Alice", "age": 30})
+ self.assertNotIn("email", parsed)
+
+ def test_to_json_with_schema_missing_required_field_raises(self) -> None:
+ d = benedict({"name": "Alice"}) # 'age' required
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ d.to_json(schema=UserSchema)
+
+ def test_to_json_without_schema(self) -> None:
+ d = benedict({"name": "Alice", "age": 30})
+ result = d.to_json()
+ self.assertIn('"name": "Alice"', result)
+
+
+class io_dict_from_toml_schema_test_case(unittest.TestCase):
+ def test_from_toml_with_schema_valid(self) -> None:
+ t = 'name = "Alice"\nage = 30\n'
+ d = IODict.from_toml(t, schema=UserSchema)
+ self.assertEqual(d, {"name": "Alice", "age": 30})
+
+ def test_from_toml_with_schema_strips_extra_fields(self) -> None:
+ t = 'name = "Alice"\nage = 30\nemail = "alice@example.com"\n'
+ d = IODict.from_toml(t, schema=UserSchema)
+ self.assertEqual(d, {"name": "Alice", "age": 30})
+ self.assertNotIn("email", d)
+
+ def test_from_toml_with_schema_missing_required_field_raises(self) -> None:
+ t = 'name = "Alice"\n' # 'age' required
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ IODict.from_toml(t, schema=UserSchema)
+
+
+class io_dict_from_xml_schema_test_case(unittest.TestCase):
+ def test_from_xml_with_schema_valid(self) -> None:
+ x = "Alice30"
+ d = IODict.from_xml(x, schema=XMLWrappedSchema)
+ self.assertEqual(d, {"root": {"name": "Alice", "age": 30}})
+
+ def test_from_xml_with_schema_strips_extra_fields(self) -> None:
+ x = "Alice30alice@example.com"
+ d = IODict.from_xml(x, schema=XMLWrappedSchema)
+ self.assertEqual(d, {"root": {"name": "Alice", "age": 30}})
+ self.assertNotIn("email", d.get("root", {}))
+
+ def test_from_xml_with_schema_missing_required_field_raises(self) -> None:
+ x = "Alice" # 'age' required
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ IODict.from_xml(x, schema=XMLWrappedSchema)
+
+
+class io_dict_from_base64_schema_test_case(unittest.TestCase):
+ def test_from_base64_with_schema_valid(self) -> None:
+ s = base64.b64encode(json.dumps({"name": "Alice", "age": 30}).encode()).decode()
+ d = IODict.from_base64(s, schema=UserSchema)
+ self.assertEqual(d, {"name": "Alice", "age": 30})
+
+ def test_from_base64_with_schema_strips_extra_fields(self) -> None:
+ s = base64.b64encode(
+ json.dumps(
+ {"name": "Alice", "age": 30, "email": "alice@example.com"}
+ ).encode()
+ ).decode()
+ d = IODict.from_base64(s, schema=UserSchema)
+ self.assertEqual(d, {"name": "Alice", "age": 30})
+ self.assertNotIn("email", d)
+
+ def test_from_base64_with_schema_missing_required_field_raises(self) -> None:
+ s = base64.b64encode(json.dumps({"name": "Alice"}).encode()).decode()
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ IODict.from_base64(s, schema=UserSchema)
+
+
+class io_dict_from_pickle_schema_test_case(unittest.TestCase):
+ def test_from_pickle_with_schema_valid(self) -> None:
+ s = base64.b64encode(
+ pickle.dumps({"name": "Alice", "age": 30}, protocol=2)
+ ).decode()
+ d = IODict.from_pickle(s, schema=UserSchema)
+ self.assertEqual(d, {"name": "Alice", "age": 30})
+
+ def test_from_pickle_with_schema_strips_extra_fields(self) -> None:
+ s = base64.b64encode(
+ pickle.dumps(
+ {"name": "Alice", "age": 30, "email": "alice@example.com"}, protocol=2
+ )
+ ).decode()
+ d = IODict.from_pickle(s, schema=UserSchema)
+ self.assertEqual(d, {"name": "Alice", "age": 30})
+ self.assertNotIn("email", d)
+
+ def test_from_pickle_with_schema_missing_required_field_raises(self) -> None:
+ s = base64.b64encode(pickle.dumps({"name": "Alice"}, protocol=2)).decode()
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ IODict.from_pickle(s, schema=UserSchema)
+
+
+class io_dict_from_plist_schema_test_case(unittest.TestCase):
+ def test_from_plist_with_schema_valid(self) -> None:
+ s = plistlib.dumps({"name": "Alice", "age": 30}).decode()
+ d = IODict.from_plist(s, schema=UserSchema)
+ self.assertEqual(d, {"name": "Alice", "age": 30})
+
+ def test_from_plist_with_schema_strips_extra_fields(self) -> None:
+ s = plistlib.dumps(
+ {"name": "Alice", "age": 30, "email": "alice@example.com"}
+ ).decode()
+ d = IODict.from_plist(s, schema=UserSchema)
+ self.assertEqual(d, {"name": "Alice", "age": 30})
+ self.assertNotIn("email", d)
+
+ def test_from_plist_with_schema_missing_required_field_raises(self) -> None:
+ s = plistlib.dumps({"name": "Alice"}).decode()
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ IODict.from_plist(s, schema=UserSchema)
+
+
+class io_dict_from_query_string_schema_test_case(unittest.TestCase):
+ def test_from_query_string_with_schema_valid(self) -> None:
+ # query strings carry all values as strings; pydantic coerces age to int
+ s = "name=Alice&age=30"
+ d = IODict.from_query_string(s, schema=UserSchema)
+ self.assertEqual(d, {"name": "Alice", "age": 30})
+ self.assertIsInstance(d["age"], int)
+
+ def test_from_query_string_with_schema_strips_extra_fields(self) -> None:
+ s = "name=Alice&age=30&email=alice%40example.com"
+ d = IODict.from_query_string(s, schema=UserSchema)
+ self.assertEqual(d, {"name": "Alice", "age": 30})
+ self.assertNotIn("email", d)
+
+ def test_from_query_string_with_schema_missing_required_field_raises(self) -> None:
+ s = "name=Alice" # 'age' required
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ IODict.from_query_string(s, schema=UserSchema)
+
+
+class io_dict_to_yaml_schema_test_case(unittest.TestCase):
+ def test_to_yaml_with_schema_valid(self) -> None:
+ d = benedict({"name": "Alice", "age": 30})
+ result = d.to_yaml(schema=UserSchema)
+ self.assertIsInstance(result, str)
+ self.assertIn("Alice", result)
+ self.assertIn("30", result)
+
+ def test_to_yaml_with_schema_strips_extra_fields(self) -> None:
+ d = benedict({"name": "Alice", "age": 30, "email": "alice@example.com"})
+ result = d.to_yaml(schema=UserSchema)
+ self.assertNotIn("email", result)
+ self.assertIn("Alice", result)
+
+ def test_to_yaml_with_schema_missing_required_field_raises(self) -> None:
+ d = benedict({"name": "Alice"}) # 'age' required
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ d.to_yaml(schema=UserSchema)
+
+
+class io_dict_to_ini_schema_test_case(unittest.TestCase):
+ def test_to_ini_with_schema_valid(self) -> None:
+ d = benedict({"name": "Alice", "age": 30})
+ result = d.to_ini(schema=UserSchema)
+ self.assertIsInstance(result, str)
+ self.assertIn("Alice", result)
+
+ def test_to_ini_with_schema_strips_extra_fields(self) -> None:
+ d = benedict({"name": "Alice", "age": 30, "email": "alice@example.com"})
+ result = d.to_ini(schema=UserSchema)
+ self.assertNotIn("email", result)
+ self.assertIn("Alice", result)
+
+ def test_to_ini_with_schema_missing_required_field_raises(self) -> None:
+ d = benedict({"name": "Alice"}) # 'age' required
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ d.to_ini(schema=UserSchema)
+
+
+class io_dict_to_toml_schema_test_case(unittest.TestCase):
+ def test_to_toml_with_schema_valid(self) -> None:
+ d = benedict({"name": "Alice", "age": 30})
+ result = d.to_toml(schema=UserSchema)
+ self.assertIsInstance(result, str)
+ self.assertIn("Alice", result)
+
+ def test_to_toml_with_schema_strips_extra_fields(self) -> None:
+ d = benedict({"name": "Alice", "age": 30, "email": "alice@example.com"})
+ result = d.to_toml(schema=UserSchema)
+ self.assertNotIn("email", result)
+ self.assertIn("Alice", result)
+
+ def test_to_toml_with_schema_missing_required_field_raises(self) -> None:
+ d = benedict({"name": "Alice"}) # 'age' required
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ d.to_toml(schema=UserSchema)
+
+
+class io_dict_to_xml_schema_test_case(unittest.TestCase):
+ def test_to_xml_with_schema_valid(self) -> None:
+ d = benedict({"root": {"name": "Alice", "age": 30}})
+ result = d.to_xml(schema=XMLWrappedSchema)
+ self.assertIsInstance(result, str)
+ self.assertIn("Alice", result)
+ self.assertIn("30", result)
+
+ def test_to_xml_with_schema_strips_extra_fields(self) -> None:
+ d = benedict(
+ {"root": {"name": "Alice", "age": 30, "email": "alice@example.com"}}
+ )
+ result = d.to_xml(schema=XMLWrappedSchema)
+ self.assertNotIn("email", result)
+ self.assertIn("Alice", result)
+
+ def test_to_xml_with_schema_missing_required_field_raises(self) -> None:
+ d = benedict({"root": {"name": "Alice"}}) # root.age required
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ d.to_xml(schema=XMLWrappedSchema)
+
+
+class io_dict_to_base64_schema_test_case(unittest.TestCase):
+ def test_to_base64_with_schema_valid(self) -> None:
+ d = benedict({"name": "Alice", "age": 30})
+ result = d.to_base64(schema=UserSchema)
+ decoded = IODict.from_base64(result)
+ self.assertEqual(dict(decoded), {"name": "Alice", "age": 30})
+
+ def test_to_base64_with_schema_strips_extra_fields(self) -> None:
+ d = benedict({"name": "Alice", "age": 30, "email": "alice@example.com"})
+ result = d.to_base64(schema=UserSchema)
+ decoded = IODict.from_base64(result)
+ self.assertEqual(dict(decoded), {"name": "Alice", "age": 30})
+ self.assertNotIn("email", decoded)
+
+ def test_to_base64_with_schema_missing_required_field_raises(self) -> None:
+ d = benedict({"name": "Alice"}) # 'age' required
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ d.to_base64(schema=UserSchema)
+
+
+class io_dict_to_pickle_schema_test_case(unittest.TestCase):
+ def test_to_pickle_with_schema_valid(self) -> None:
+ d = benedict({"name": "Alice", "age": 30})
+ result = d.to_pickle(schema=UserSchema)
+ decoded = IODict.from_pickle(result)
+ self.assertEqual(dict(decoded), {"name": "Alice", "age": 30})
+
+ def test_to_pickle_with_schema_strips_extra_fields(self) -> None:
+ d = benedict({"name": "Alice", "age": 30, "email": "alice@example.com"})
+ result = d.to_pickle(schema=UserSchema)
+ decoded = IODict.from_pickle(result)
+ self.assertEqual(dict(decoded), {"name": "Alice", "age": 30})
+ self.assertNotIn("email", decoded)
+
+ def test_to_pickle_with_schema_missing_required_field_raises(self) -> None:
+ d = benedict({"name": "Alice"}) # 'age' required
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ d.to_pickle(schema=UserSchema)
+
+
+class io_dict_to_plist_schema_test_case(unittest.TestCase):
+ def test_to_plist_with_schema_valid(self) -> None:
+ d = benedict({"name": "Alice", "age": 30})
+ result = d.to_plist(schema=UserSchema)
+ decoded = IODict.from_plist(result)
+ self.assertEqual(dict(decoded), {"name": "Alice", "age": 30})
+
+ def test_to_plist_with_schema_strips_extra_fields(self) -> None:
+ d = benedict({"name": "Alice", "age": 30, "email": "alice@example.com"})
+ result = d.to_plist(schema=UserSchema)
+ decoded = IODict.from_plist(result)
+ self.assertEqual(dict(decoded), {"name": "Alice", "age": 30})
+ self.assertNotIn("email", decoded)
+
+ def test_to_plist_with_schema_missing_required_field_raises(self) -> None:
+ d = benedict({"name": "Alice"}) # 'age' required
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ d.to_plist(schema=UserSchema)
+
+
+class io_dict_to_query_string_schema_test_case(unittest.TestCase):
+ def test_to_query_string_with_schema_valid(self) -> None:
+ d = benedict({"name": "Alice", "age": 30})
+ result = d.to_query_string(schema=UserSchema)
+ self.assertIsInstance(result, str)
+ self.assertIn("Alice", result)
+
+ def test_to_query_string_with_schema_strips_extra_fields(self) -> None:
+ d = benedict({"name": "Alice", "age": 30, "email": "alice@example.com"})
+ result = d.to_query_string(schema=UserSchema)
+ self.assertNotIn("email", result)
+ self.assertIn("Alice", result)
+
+ def test_to_query_string_with_schema_missing_required_field_raises(self) -> None:
+ d = benedict({"name": "Alice"}) # 'age' required
+ with self.assertRaises((pydantic.ValidationError, ValueError)):
+ d.to_query_string(schema=UserSchema)