Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forbid extra option and bubbling up the inner-most exception #183

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 103 additions & 81 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,82 +27,86 @@ but it also does it _super quick_.

Table of contents
-------------------------------------------------------------------------------
* [Introduction](#introduction)
* [Installation](#installation)
* [Changelog](#changelog)
* [Supported data types](#supported-data-types)
* [Usage example](#usage-example)
* [How does it work?](#how-does-it-work)
* [Benchmark](#benchmark)
* [Supported serialization formats](#supported-serialization-formats)
* [Basic form](#basic-form)
* [JSON](#json)
* [YAML](#yaml)
* [TOML](#toml)
* [MessagePack](#messagepack)
* [Customization](#customization)
* [`SerializableType` interface](#serializabletype-interface)
* [User-defined types](#user-defined-types)
* [User-defined generic types](#user-defined-generic-types)
* [`SerializationStrategy`](#serializationstrategy)
* [Third-party types](#third-party-types)
* [Third-party generic types](#third-party-generic-types)
* [Field options](#field-options)
* [`serialize` option](#serialize-option)
* [`deserialize` option](#deserialize-option)
* [`serialization_strategy` option](#serialization_strategy-option)
* [`alias` option](#alias-option)
* [Config options](#config-options)
* [`debug` config option](#debug-config-option)
* [`code_generation_options` config option](#code_generation_options-config-option)
* [`serialization_strategy` config option](#serialization_strategy-config-option)
* [`aliases` config option](#aliases-config-option)
* [`serialize_by_alias` config option](#serialize_by_alias-config-option)
* [`allow_deserialization_not_by_alias` config option](#allow_deserialization_not_by_alias-config-option)
* [`omit_none` config option](#omit_none-config-option)
* [`omit_default` config option](#omit_default-config-option)
* [`namedtuple_as_dict` config option](#namedtuple_as_dict-config-option)
* [`allow_postponed_evaluation` config option](#allow_postponed_evaluation-config-option)
* [`dialect` config option](#dialect-config-option)
* [`orjson_options` config option](#orjson_options-config-option)
* [`discriminator` config option](#discriminator-config-option)
* [`lazy_compilation` config option](#lazy_compilation-config-option)
* [`sort_keys` config option](#sort_keys-config-option)
* [Passing field values as is](#passing-field-values-as-is)
* [Extending existing types](#extending-existing-types)
* [Dialects](#dialects)
* [`serialization_strategy` dialect option](#serialization_strategy-dialect-option)
* [`serialize_by_alias` dialect option](#serialize_by_alias-dialect-option)
* [`omit_none` dialect option](#omit_none-dialect-option)
* [`omit_default` dialect option](#omit_default-dialect-option)
* [`named_tuple_as_dict` dialect option](#namedtuple_as_dict-dialect-option)
* [`no_copy_collections` dialect option](#no_copy_collections-dialect-option)
* [Changing the default dialect](#changing-the-default-dialect)
* [Discriminator](#discriminator)
* [Subclasses distinguishable by a field](#subclasses-distinguishable-by-a-field)
* [Subclasses without a common field](#subclasses-without-a-common-field)
* [Class level discriminator](#class-level-discriminator)
* [Working with union of classes](#working-with-union-of-classes)
* [Using a custom variant tagger function](#using-a-custom-variant-tagger-function)
* [Code generation options](#code-generation-options)
* [Add `omit_none` keyword argument](#add-omit_none-keyword-argument)
* [Add `by_alias` keyword argument](#add-by_alias-keyword-argument)
* [Add `dialect` keyword argument](#add-dialect-keyword-argument)
* [Add `context` keyword argument](#add-context-keyword-argument)
* [Generic dataclasses](#generic-dataclasses)
* [Generic dataclass inheritance](#generic-dataclass-inheritance)
* [Generic dataclass in a field type](#generic-dataclass-in-a-field-type)
* [`GenericSerializableType` interface](#genericserializabletype-interface)
* [Serialization hooks](#serialization-hooks)
* [Before deserialization](#before-deserialization)
* [After deserialization](#after-deserialization)
* [Before serialization](#before-serialization)
* [After serialization](#after-serialization)
* [JSON Schema](#json-schema)
* [Building JSON Schema](#building-json-schema)
* [JSON Schema constraints](#json-schema-constraints)
* [Extending JSON Schema](#extending-json-schema)
* [JSON Schema and custom serialization methods](#json-schema-and-custom-serialization-methods)
- [Table of contents](#table-of-contents)
- [Introduction](#introduction)
- [Installation](#installation)
- [Changelog](#changelog)
- [Supported data types](#supported-data-types)
- [Usage example](#usage-example)
- [How does it work?](#how-does-it-work)
- [Benchmark](#benchmark)
- [Supported serialization formats](#supported-serialization-formats)
- [Basic form](#basic-form)
- [JSON](#json)
- [json library](#json-library)
- [orjson library](#orjson-library)
- [YAML](#yaml)
- [TOML](#toml)
- [MessagePack](#messagepack)
- [Customization](#customization)
- [SerializableType interface](#serializabletype-interface)
- [User-defined types](#user-defined-types)
- [User-defined generic types](#user-defined-generic-types)
- [SerializationStrategy](#serializationstrategy)
- [Third-party types](#third-party-types)
- [Third-party generic types](#third-party-generic-types)
- [Field options](#field-options)
- [`serialize` option](#serialize-option)
- [`deserialize` option](#deserialize-option)
- [`serialization_strategy` option](#serialization_strategy-option)
- [`alias` option](#alias-option)
- [Config options](#config-options)
- [`debug` config option](#debug-config-option)
- [`code_generation_options` config option](#code_generation_options-config-option)
- [`serialization_strategy` config option](#serialization_strategy-config-option)
- [`aliases` config option](#aliases-config-option)
- [`serialize_by_alias` config option](#serialize_by_alias-config-option)
- [`allow_deserialization_not_by_alias` config option](#allow_deserialization_not_by_alias-config-option)
- [`omit_none` config option](#omit_none-config-option)
- [`omit_default` config option](#omit_default-config-option)
- [`namedtuple_as_dict` config option](#namedtuple_as_dict-config-option)
- [`allow_postponed_evaluation` config option](#allow_postponed_evaluation-config-option)
- [`dialect` config option](#dialect-config-option)
- [`orjson_options` config option](#orjson_options-config-option)
- [`discriminator` config option](#discriminator-config-option)
- [`lazy_compilation` config option](#lazy_compilation-config-option)
- [`sort_keys` config option](#sort_keys-config-option)
- [`forbid_extra_keys` config option](#forbid_extra_keys-config-option)
- [Passing field values as is](#passing-field-values-as-is)
- [Extending existing types](#extending-existing-types)
- [Dialects](#dialects)
- [`serialization_strategy` dialect option](#serialization_strategy-dialect-option)
- [`serialize_by_alias` dialect option](#serialize_by_alias-dialect-option)
- [`omit_none` dialect option](#omit_none-dialect-option)
- [`omit_default` dialect option](#omit_default-dialect-option)
- [`namedtuple_as_dict` dialect option](#namedtuple_as_dict-dialect-option)
- [`no_copy_collections` dialect option](#no_copy_collections-dialect-option)
- [Changing the default dialect](#changing-the-default-dialect)
- [Discriminator](#discriminator)
- [Subclasses distinguishable by a field](#subclasses-distinguishable-by-a-field)
- [Subclasses without a common field](#subclasses-without-a-common-field)
- [Class level discriminator](#class-level-discriminator)
- [Working with union of classes](#working-with-union-of-classes)
- [Using a custom variant tagger function](#using-a-custom-variant-tagger-function)
- [Code generation options](#code-generation-options)
- [Add `omit_none` keyword argument](#add-omit_none-keyword-argument)
- [Add `by_alias` keyword argument](#add-by_alias-keyword-argument)
- [Add `dialect` keyword argument](#add-dialect-keyword-argument)
- [Add `context` keyword argument](#add-context-keyword-argument)
- [Generic dataclasses](#generic-dataclasses)
- [Generic dataclass inheritance](#generic-dataclass-inheritance)
- [Generic dataclass in a field type](#generic-dataclass-in-a-field-type)
- [GenericSerializableType interface](#genericserializabletype-interface)
- [Serialization hooks](#serialization-hooks)
- [Before deserialization](#before-deserialization)
- [After deserialization](#after-deserialization)
- [Before serialization](#before-serialization)
- [After serialization](#after-serialization)
- [JSON Schema](#json-schema)
- [Building JSON Schema](#building-json-schema)
- [JSON Schema constraints](#json-schema-constraints)
- [Extending JSON Schema](#extending-json-schema)
- [JSON Schema and custom serialization methods](#json-schema-and-custom-serialization-methods)

Introduction
-------------------------------------------------------------------------------
Expand Down Expand Up @@ -148,8 +152,8 @@ For convenience, there is a table below that outlines the
last version of `mashumaro` that can be installed on unmaintained versions
of Python.

| Python Version | Last Version of mashumaro | Python EOL |
|----------------|--------------------------------------------------------------------|------------|
| Python Version | Last Version of mashumaro | Python EOL |
| -------------- | ------------------------------------------------------------------ | ---------- |
| 3.7 | [3.9.1](https://github.com/Fatal1ty/mashumaro/releases/tag/v3.9.1) | 2023-06-27 |
| 3.6 | [3.1.1](https://github.com/Fatal1ty/mashumaro/releases/tag/v3.1.1) | 2021-12-23 |

Expand Down Expand Up @@ -1127,7 +1131,7 @@ that all possible engines depend on the data type that this option is used
with. At this moment there are next serialization engines to choose from:

| Applicable data types | Supported engines | Description |
|:---------------------------|:---------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| :------------------------- | :------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NamedTuple`, `namedtuple` | `as_list`, `as_dict` | How to pack named tuples. By default `as_list` engine is used that means your named tuple class instance will be packed into a list of its values. You can pack it into a dictionary using `as_dict` engine. |
| `Any` | `omit` | Skip the field during serialization |

Expand Down Expand Up @@ -1172,7 +1176,7 @@ that all possible engines depend on the data type that this option is used
with. At this moment there are next deserialization engines to choose from:

| Applicable data types | Supported engines | Description |
|:---------------------------|:------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| :------------------------- | :---------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `datetime`, `date`, `time` | [`ciso8601`](https://github.com/closeio/ciso8601#supported-subset-of-iso-8601), [`pendulum`](https://github.com/sdispater/pendulum) | How to parse datetime string. By default native [`fromisoformat`](https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat) of corresponding class will be used for `datetime`, `date` and `time` fields. It's the fastest way in most cases, but you can choose an alternative. |
| `NamedTuple`, `namedtuple` | `as_list`, `as_dict` | How to unpack named tuples. By default `as_list` engine is used that means your named tuple class instance will be created from a list of its values. You can unpack it from a dictionary using `as_dict` engine. |

Expand Down Expand Up @@ -1317,7 +1321,7 @@ The following table provides a brief overview of all the available constants
described below.

| Constant | Description |
|:----------------------------------------------------------------|:---------------------------------------------------------------------|
| :-------------------------------------------------------------- | :------------------------------------------------------------------- |
| [`TO_DICT_ADD_OMIT_NONE_FLAG`](#add-omit_none-keyword-argument) | Adds `omit_none` keyword-only argument to `to_*` methods. |
| [`TO_DICT_ADD_BY_ALIAS_FLAG`](#add-by_alias-keyword-argument) | Adds `by_alias` keyword-only argument to `to_*` methods. |
| [`ADD_DIALECT_SUPPORT`](#add-dialect-keyword-argument) | Adds `dialect` keyword-only argument to `from_*` and `to_*` methods. |
Expand Down Expand Up @@ -1713,6 +1717,24 @@ t = SortedDataClass(1, 2)
assert t.to_dict() == {"bar": 2, "foo": 1}
```

#### `forbid_extra_keys` config option

When set, the deserialization of dataclasses will fail if the input dictionary contains keys that are not present in the dataclass.

```python
from dataclasses import dataclass

from mashumaro import DataClassDictMixin

@dataclass
class DataClass(DataClassDictMixin):
a: int

DataClass.from_dict({"a": 1, "b": 2}) # ExtraKeysError: Extra keys: {'b'}
```

It plays well with `aliases` and `allow_deserialization_not_by_alias` options.

### Passing field values as is

In some cases it's needed to pass a field value as is without any changes
Expand Down
1 change: 1 addition & 0 deletions mashumaro/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,4 @@ class BaseConfig:
lazy_compilation: bool = False
sort_keys: bool = False
allow_deserialization_not_by_alias: bool = False
forbid_extra_keys: bool = False
61 changes: 55 additions & 6 deletions mashumaro/core/meta/code/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
InvalidFieldValue,
MissingDiscriminatorError,
MissingField,
ExtraKeysError,
SuitableVariantNotFoundError,
ThirdPartyModuleNotFoundError,
UnresolvedTypeReferenceError,
Expand Down Expand Up @@ -439,15 +440,43 @@ def _add_unpack_method_lines(self, method_name: str) -> None:
else:
missing_kw_only = True
kw_only_fields.add(fname)
filtered_fields.append((fname, ftype))

metadata = self.metadatas.get(fname, {})
alias = metadata.get("alias")
if alias is None:
alias = config.aliases.get(fname)

filtered_fields.append((fname, alias, ftype))
if filtered_fields:
if config.forbid_extra_keys:
allowed_keys = {f[1] or f[0] for f in filtered_fields}

# If a discirimator withg a field is set via config,
# we should allow this field to be present in the input
# This will not work for annotated discriminators though...
discr = self.get_config(look_in_parents=True).discriminator
if discr and discr.field:
allowed_keys.add(discr.field)

if config.allow_deserialization_not_by_alias:
allowed_keys |= {f[0] for f in filtered_fields}

allowed_keys_str = "'" + "', '".join(allowed_keys) + "'"

self.add_line("d_keys = set(d.keys())")
self.add_line(
f"forbidden_keys = d_keys - {{{allowed_keys_str}}}"
)
with self.indent("if forbidden_keys:"):
self.add_line(
"raise ExtraKeysError(forbidden_keys,cls) "
"from None"
)

with self.indent("try:"):
for fname, ftype in filtered_fields:
for fname, alias, ftype in filtered_fields:
self.add_type_modules(ftype)
metadata = self.metadatas.get(fname, {})
alias = metadata.get("alias")
if alias is None:
alias = config.aliases.get(fname)
field_block = FieldUnpackerCodeBlockBuilder(
self, self.lines.branch_off()
).build(
Expand Down Expand Up @@ -1225,7 +1254,27 @@ def _try_set_value(
) -> None:
with self.lines.indent("try:"):
self._set_value(field_name, unpacked_value, in_kwargs)
with self.lines.indent("except:"):
with self.lines.indent("except MissingField as inner:"):
self.lines.append(
f"path = '{field_name}' + '.' + inner.path "
f"if inner.path else '{field_name}'"
)
self.lines.append(
"raise MissingField("
"inner.field_name,inner.field_type,"
"inner.holder_class,path) from None"
)
with self.lines.indent("except InvalidFieldValue as inner:"):
self.lines.append(
f"path = '{field_name}' + '.' + inner.path "
f"if inner.path else '{field_name}'"
)
self.lines.append(
"raise InvalidFieldValue("
"inner.field_name,inner.field_type,inner.field_value,"
"inner.holder_class,inner.msg,path) from None"
)
with self.lines.indent("except Exception:"):
self.lines.append(
"raise InvalidFieldValue("
f"'{field_name}',{field_type_name},value,cls)"
Expand Down
Loading