diff --git a/docs/source/pages/misc.rst b/docs/source/pages/misc.rst index 7912e87..58925e5 100644 --- a/docs/source/pages/misc.rst +++ b/docs/source/pages/misc.rst @@ -71,6 +71,16 @@ The following example illustrate how to serialize ``xs:list`` element: :language: python +JSON only field +~~~~~~~~~~~~~~~ + +To prevent a field from serialization/deserialization only for xml format +mark it with :py:class:`pydantic_xml.NoXml` annotation: + +.. literalinclude:: ../../../examples/snippets/py3.9/noxml.py + :language: python + + Optional type encoding ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/examples/snippets/py3.9/noxml.py b/examples/snippets/py3.9/noxml.py new file mode 100644 index 0000000..dbd462d --- /dev/null +++ b/examples/snippets/py3.9/noxml.py @@ -0,0 +1,29 @@ +from typing import Annotated, Optional +from xml.etree.ElementTree import canonicalize + +from pydantic_xml import BaseXmlModel, NoXml, element + + +class Company(BaseXmlModel): + title: str = element() + website: Annotated[Optional[str], NoXml] = element(default=None) + + +xml_doc = ''' + + SpaceX + +''' + +company = Company.from_xml(xml_doc) + +assert canonicalize(company.to_xml(), strip_text=True) == canonicalize(xml_doc, strip_text=True) + +json_doc = ''' +{ + "title": "SpaceX", + "website": "https://spacex.com/" +} +''' +company = Company.model_validate_json(json_doc) +assert company.model_dump_json(indent=4) == json_doc.strip() diff --git a/pydantic_xml/__init__.py b/pydantic_xml/__init__.py index ca5e165..f749be4 100644 --- a/pydantic_xml/__init__.py +++ b/pydantic_xml/__init__.py @@ -4,8 +4,8 @@ from . import config, errors, model from .errors import ModelError, ParsingError -from .fields import XmlFieldSerializer, XmlFieldValidator, attr, computed_attr, computed_element, element, wrapped -from .fields import xml_field_serializer, xml_field_validator +from .fields import NoXml, XmlFieldSerializer, XmlFieldValidator, attr, computed_attr, computed_element, element +from .fields import wrapped, xml_field_serializer, xml_field_validator from .model import BaseXmlModel, RootXmlModel, create_model __all__ = ( @@ -23,6 +23,7 @@ 'model', 'xml_field_serializer', 'xml_field_validator', + 'NoXml', 'XmlFieldValidator', 'XmlFieldSerializer', ) diff --git a/pydantic_xml/fields.py b/pydantic_xml/fields.py index 3139662..9f916b7 100644 --- a/pydantic_xml/fields.py +++ b/pydantic_xml/fields.py @@ -21,6 +21,7 @@ 'xml_field_serializer', 'xml_field_validator', 'ComputedXmlEntityInfo', + 'NoXml', 'XmlEntityInfo', 'XmlEntityInfoP', 'XmlFieldSerializer', @@ -28,6 +29,12 @@ ) +class NoXml: + """ + Annotation to prevent field encoding or decoding for xml format only. + """ + + class XmlEntityInfoP(typing.Protocol): location: Optional[EntityLocation] path: Optional[str] diff --git a/pydantic_xml/serializers/factories/model.py b/pydantic_xml/serializers/factories/model.py index dcb77df..5c4ecb5 100644 --- a/pydantic_xml/serializers/factories/model.py +++ b/pydantic_xml/serializers/factories/model.py @@ -9,7 +9,7 @@ import pydantic_xml as pxml from pydantic_xml import errors, utils from pydantic_xml.element import XmlElementReader, XmlElementWriter, is_element_nill, make_element_nill -from pydantic_xml.fields import ComputedXmlEntityInfo, XmlEntityInfoP, extract_field_xml_entity_info +from pydantic_xml.fields import ComputedXmlEntityInfo, NoXml, XmlEntityInfoP, extract_field_xml_entity_info from pydantic_xml.serializers.serializer import SearchMode, Serializer from pydantic_xml.typedefs import EntityLocation, Location, NsMap from pydantic_xml.utils import QName, merge_nsmaps, select_ns @@ -76,6 +76,10 @@ def from_core_schema(cls, schema: pcs.ModelSchema, ctx: Serializer.Context) -> ' fields_validation_aliases: Dict[str, str] = {} fields_serializers: Dict[str, Serializer] = {} for field_name, model_field in fields_schema['fields'].items(): + field_info = model_cls.model_fields[field_name] + if NoXml in field_info.metadata: + continue + if model_field.get('serialization_exclude', False): fields_serialization_exclude.add(field_name) @@ -84,7 +88,6 @@ def from_core_schema(cls, schema: pcs.ModelSchema, ctx: Serializer.Context) -> ' if isinstance(validation_alias, str): fields_validation_aliases[field_name] = validation_alias - field_info = model_cls.model_fields[field_name] field_ctx = ctx.child( field_name=field_name, field_alias=field_alias, diff --git a/tests/test_misc.py b/tests/test_misc.py index 52b910c..88b83bc 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -6,7 +6,7 @@ import pytest from helpers import assert_xml_equal -from pydantic_xml import BaseXmlModel, RootXmlModel, attr, element, errors, wrapped +from pydantic_xml import BaseXmlModel, NoXml, RootXmlModel, attr, element, errors, wrapped def test_xml_declaration(): @@ -441,3 +441,27 @@ class TestModel(BaseXmlModel, tag="model"): assert not error_value_input, error_str else: assert error_value_input, error_str + + +@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python 3.9 and above") +def test_no_xml_annotation(): + from typing import Annotated + + class TestModel(BaseXmlModel, tag="model"): + field1: str = element() + field2: Annotated[Optional[str], NoXml] = element(default=None) + + expected_xml = 'data1' + actual_obj = TestModel.from_xml(expected_xml) + expected_obj = TestModel(field1='data1') + + assert actual_obj == expected_obj + actual_xml = actual_obj.to_xml(skip_empty=True) + assert_xml_equal(actual_xml, expected_xml.encode()) + + expected_json = {'field1': 'data1', 'field2': 'data2'} + actual_obj = TestModel.model_validate(expected_json) + expected_obj = TestModel(field1='data1', field2='data2') + assert actual_obj == expected_obj + + assert actual_obj.model_dump() == expected_json