Repository: s-knibbs/dataclasses-jsonschema
Pull request #193 (2022-10-05 16:35:33)

Performance improvements
Performance improvements - mainly through optional native libraries for field encoding dates and uuids. Python 3.10 support.


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7302b5b..bd95383 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,7 +5,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [3.7, 3.8, 3.9]
+        python-version: ['3.7', '3.8', '3.9', '3.10']
 
     steps:
       - uses: actions/checkout@v1
@@ -18,4 +18,4 @@ jobs:
           python -m pip install --upgrade pip
           pip install tox tox-gh-actions
       - name: Run tox
-        run: tox
+        run: tox --parallel
diff --git a/README.rst b/README.rst
index a403fff..4127890 100644
--- a/README.rst
+++ b/README.rst
@@ -36,6 +36,21 @@ For improved validation performance using `fastjsonschema <https://github.com/ho
 
     ~$ pip install dataclasses-jsonschema[fast-validation]
 
+For improved uuid performance using `fastuuid <https://pypi.org/project/fastuuid/>`_, install with:
+
+.. code:: bash
+
+    ~$ pip install dataclasses-jsonschema[fast-uuid]
+
+For improved date and datetime parsing performance using `ciso8601 <https://pypi.org/project/ciso8601/>`_, install with:
+
+.. code:: bash
+
+    ~$ pip install dataclasses-jsonschema[fast-dateparsing]
+
+Beware `ciso8601` doesn’t support the entirety of the ISO 8601 spec, only a popular subset.
+
+
 Examples
 --------
 
diff --git a/dataclasses_jsonschema/__init__.py b/dataclasses_jsonschema/__init__.py
index 929987f..2ed85a7 100644
--- a/dataclasses_jsonschema/__init__.py
+++ b/dataclasses_jsonschema/__init__.py
@@ -4,12 +4,12 @@ import sys
 import functools
 from decimal import Decimal
 from ipaddress import IPv4Address, IPv6Address
-from typing import Optional, Type, Union, Any, Dict, Tuple, List, Callable, TypeVar
+from typing import Optional, Type, Union, Any, Dict, Tuple, List, Callable, TypeVar, ClassVar
 import re
 from datetime import datetime, date
 from dataclasses import fields, is_dataclass, Field, MISSING, dataclass, asdict
-from uuid import UUID
 from enum import Enum
+from uuid import UUID as SlowUUID
 import warnings
 
 try:
@@ -17,12 +17,18 @@ try:
 except ImportError:
     from typing_extensions import Final, Literal  # type: ignore
 
+try:
+    from fastuuid import UUID as FastUUID
+
+    have_fastuuid = True
+except ImportError:
+    have_fastuuid = False
 
 from .field_types import (  # noqa: F401
     FieldEncoder, DateFieldEncoder, DateTimeFieldEncoder, UuidField, DecimalField,
     IPv4AddressField, IPv6AddressField, DateTimeField
 )
-from .type_defs import JsonDict, SchemaType, JsonSchemaMeta, _NULL_TYPE, NULL  # noqa: F401
+from .type_defs import JsonDict, NoneType, SchemaType, JsonSchemaMeta, _NULL_TYPE, NULL  # noqa: F401
 from .type_hints import get_class_type_hints
 
 try:
@@ -54,6 +60,12 @@ SEQUENCE_TYPES = {
 MAPPING_TYPES = ('Dict', 'Mapping', 'dict')
 TUPLE_TYPES = ('Tuple', 'tuple')
 
+PRIMITIVES: Tuple[Type, ...] = (int, str, float, bool, type(None))
+
+IS_PYTHON_36 = sys.version_info[:2] == (3, 6)
+IS_PYTHON_37_PLUS = sys.version_info[:2] >= (3, 7)
+IS_PYTHON_310_PLUS = sys.version_info[:2] >= (3, 10)
+
 
 class ValidationError(Exception):
     pass
@@ -84,7 +96,7 @@ def issubclass_safe(klass: Any, base: Union[Type, Tuple[Type, ...]]):
 
 def is_optional(field: Any) -> bool:
     try:
-        return field.__origin__ == Union and type(None) in field.__args__
+        return field.__origin__ == Union and NoneType in field.__args__
     except AttributeError:
         return False
 
@@ -93,7 +105,7 @@ def is_final(field: Any) -> bool:
     try:
         return field.__origin__ == Final
     except AttributeError:
-        if sys.version_info[:2] == (3, 6):
+        if IS_PYTHON_36:
             return type(field).__qualname__ == "_Final"
         return False
 
@@ -102,14 +114,14 @@ def is_literal(field: Any) -> bool:
     try:
         return field.__origin__ == Literal
     except AttributeError:
-        if sys.version_info[:2] == (3, 6):
+        if IS_PYTHON_36:
             return type(field).__qualname__ == "_Literal"
         return False
 
 
 def is_nullable(field: Any) -> bool:
     try:
-        if sys.version_info[:2] == (3, 6):
+        if IS_PYTHON_36:
             # Hack to get python 3.6 working
             return "_NULL_TYPE" in repr(field)
         return field.__origin__ == Union and _NULL_TYPE in field.__args__
@@ -118,19 +130,21 @@ def is_nullable(field: Any) -> bool:
 
 
 def unwrap_final(final_type: Any) -> Any:
-    return final_type.__args__[0] if sys.version_info[:2] >= (3, 7) else final_type.__type__
+    return final_type.__args__[0] if IS_PYTHON_37_PLUS else final_type.__type__
 
 
 def unwrap_optional(optional_type: Any) -> Any:
-    idx = optional_type.__args__.index(type(None))
-    return Union[optional_type.__args__[:idx] + optional_type.__args__[idx+1:]]
+    args = optional_type.__args__
+    idx = args.index(NoneType)
+    return Union[args[:idx] + args[idx+1:]]
 
 
 def unwrap_nullable(nullable_type: Any) -> Any:
-    if sys.version_info[:2] == (3, 6):
-        return Union[nullable_type.__args__]
-    idx = nullable_type.__args__.index(_NULL_TYPE)
-    return Union[nullable_type.__args__[:idx] + nullable_type.__args__[idx+1:]]
+    args = nullable_type.__args__
+    if IS_PYTHON_36:
+        return Union[args]
+    idx = args.index(_NULL_TYPE)
+    return Union[args[:idx] + args[idx+1:]]
 
 
 def schema_reference(schema_type: SchemaType, schema_name: str) -> Dict[str, str]:
@@ -148,6 +162,17 @@ FieldExcludeList = Tuple[Union[str, Tuple[str, 'FieldExcludeList']], ...]  # typ
 
 DEFAULT_SCHEMA_TYPE = SchemaType.DRAFT_06
 
+_jsonschema_mixin_field_encoders: Dict[Type, FieldEncoder] = {
+    date: DateFieldEncoder(),
+    datetime: DateTimeFieldEncoder(),
+    SlowUUID: UuidField(),
+    Decimal: DecimalField(),
+    IPv4Address: IPv4AddressField(),
+    IPv6Address: IPv6AddressField(),
+}
+if have_fastuuid:
+    _jsonschema_mixin_field_encoders[FastUUID] = UuidField()
+
 
 @functools.lru_cache()
 def _to_camel_case(value: str) -> str:
@@ -158,6 +183,35 @@ def _to_camel_case(value: str) -> str:
         return value
 
 
+# statically defined codecs can run faster outside of classmethods
+def _encoder_is_json_schema_subclass(_, v, o):
+    # Only need to validate at the top level
+    return v.to_dict(omit_none=o, validate=False)
+
+
+def _decoder_is_json_schema_subclass(_, ft, val):
+    return ft.from_dict(val, validate=False)
+
+
+def _decoder_identity(_, __, val):
+    return val
+
+
+def _decoder_to_ft(_, ft, val):
+    return ft(val)
+
+
+def _encoder_is_enum(_, v, __):
+    try:
+        return v.value
+    except AttributeError as e:
+        raise UnknownEnumValueError(f'Unknown enum value: {v}') from e
+
+
+def _encoder_identity(_, v, __):
+    return v
+
+
 @dataclass(frozen=True)
 class SchemaOptions:
     schema_type: SchemaType
@@ -203,28 +257,22 @@ class JsonSchemaMixin:
     """Mixin which adds methods to generate a JSON schema and
     convert to and from JSON encodable dicts with validation against the schema
     """
-    _field_encoders: Dict[Type, FieldEncoder] = {
-        date: DateFieldEncoder(),
-        datetime: DateTimeFieldEncoder(),
-        UUID: UuidField(),
-        Decimal: DecimalField(),
-        IPv4Address: IPv4AddressField(),
-        IPv6Address: IPv6AddressField()
-    }
+
+    _field_encoders: ClassVar[Dict[Type, FieldEncoder]] = _jsonschema_mixin_field_encoders
 
     # Cache of the generated schema
-    __schema: Dict[SchemaOptions, JsonDict]
-    __compiled_schema: Dict[SchemaOptions, Callable]
-    __definitions: Dict[SchemaOptions, JsonDict]
+    __schema: ClassVar[Dict[SchemaOptions, JsonDict]]
+    __compiled_schema: ClassVar[Dict[SchemaOptions, Callable]]
+    __definitions: ClassVar[Dict[SchemaOptions, JsonDict]]
     # Cache of field encode / decode functions
-    __encode_cache: Dict[Any, _ValueEncoder]
-    __decode_cache: Dict[Any, _ValueDecoder]
-    __mapped_fields: List[JsonSchemaField]
-    __discriminator_name: Optional[str]
+    __encode_cache: ClassVar[Dict[Any, _ValueEncoder]]
+    __decode_cache: ClassVar[Dict[Any, _ValueDecoder]]
+    __mapped_fields: ClassVar[List[JsonSchemaField]]
+    __discriminator_name: ClassVar[Optional[str]]
     # True if __discriminator_name is inherited from the base class
-    __discriminator_inherited: bool
-    __allow_additional_props: bool
-    __serialise_properties: Union[Tuple[str, ...], bool]
+    __discriminator_inherited: ClassVar[bool]
+    __allow_additional_props: ClassVar[bool]
+    __serialise_properties: ClassVar[Union[Tuple[str, ...], bool]]
 
     @classmethod
     def _discriminator(cls) -> Optional[str]:
@@ -307,26 +355,21 @@ class JsonSchemaMixin:
             elif is_final(field_type):
                 def encoder(ft, val, o): return cls._encode_field(unwrap_final(ft), val, o)
             elif is_enum(field_type):
-                def encoder(_, v, __):
-                    try:
-                        return v.value
-                    except AttributeError as e:
-                        raise UnknownEnumValueError(f'Unknown enum value: {v}') from e
+                encoder = _encoder_is_enum
             elif field_type_name == 'Union':
                 # Attempt to encode the field with each union variant.
                 # TODO: Find a more reliable method than this since in the case 'Union[List[str], Dict[str, int]]' this
                 # will just output the dict keys as a list
                 encoded = None
                 # Remove primitive types from unions, these are handled later
-                primitives = (int, str, float, bool, type(None))
-                field_args = filter(lambda x: not issubclass_safe(x, primitives), field_type.__args__)
+                field_args = filter(lambda x: not issubclass_safe(x, PRIMITIVES), field_type.__args__)
                 for variant in field_args:
                     try:
                         encoded = cls._encode_field(variant, value, omit_none)
                         break
                     except (TypeError, AttributeError, UnknownEnumValueError):
                         continue
-                if encoded is None and isinstance(value, primitives) and type(value) in field_type.__args__:
+                if encoded is None and isinstance(value, PRIMITIVES) and type(value) in field_type.__args__:
                     encoded = cls._encode_field(type(value), value, omit_none)
                 if encoded is None:
                     raise TypeError("No variant of '{}' matched the type '{}'".format(field_type, type(value)))
@@ -349,13 +392,13 @@ class JsonSchemaMixin:
                 def encoder(ft, val, o):
                     return [cls._encode_field(ft.__args__[idx], v, o) for idx, v in enumerate(val)]
             elif cls._is_json_schema_subclass(field_type):
-                # Only need to validate at the top level
-                def encoder(_, v, o): return v.to_dict(omit_none=o, validate=False)
+                encoder = _encoder_is_json_schema_subclass
             elif hasattr(field_type, "__supertype__"):  # NewType field
                 def encoder(ft, v, o):
                     return cls._encode_field(ft.__supertype__, v, o)
             else:
-                def encoder(_, v, __): return v
+                encoder = _encoder_identity
+
             cls.__encode_cache[field_type] = encoder  # type: ignore
         return encoder(field_type, value, omit_none)
 
@@ -388,7 +431,10 @@ class JsonSchemaMixin:
                 members = inspect.getmembers(cls, inspect.isdatadescriptor)
                 for name, member in members:
                     if name != "__weakref__" and (include_properties is None or name in include_properties):
-                        f = Field(MISSING, None, None, None, None, None, None)
+                        if IS_PYTHON_310_PLUS:
+                            f = Field(MISSING, None, None, None, None, None, None, kw_only=False)
+                        else:
+                            f = Field(MISSING, None, None, None, None, None, None)
                         f.name = name
                         f.type = member.fget.__annotations__['return']
                         mapped_fields.append(JsonSchemaField(f, name, is_property=True))
@@ -447,11 +493,11 @@ class JsonSchemaMixin:
             # Note: Only literal types composed of primitive values are currently supported
             if type(value) in JSON_ENCODABLE_TYPES and (field_type in JSON_ENCODABLE_TYPES or is_literal(field_type)):
                 if is_literal(field_type):
-                    def decoder(_, __, val): return val
+                    decoder = _decoder_identity
                 else:
-                    def decoder(_, ft, val): return ft(val)
+                    decoder = _decoder_to_ft
             elif cls._is_json_schema_subclass(field_type):
-                def decoder(_, ft, val): return ft.from_dict(val, validate=False)
+                decoder = _decoder_is_json_schema_subclass
             elif is_nullable(field_type):
                 def decoder(f, ft, val): return cls._decode_field(f, unwrap_nullable(ft), val)
             elif is_optional(field_type):
@@ -462,8 +508,7 @@ class JsonSchemaMixin:
                 # Attempt to decode the value using each decoder in turn
                 decoded = None
                 # Remove primitive types from unions, these are handled later
-                primitives = (int, str, float, bool, type(None))
-                field_args = filter(lambda x: not issubclass_safe(x, primitives), field_type.__args__)
+                field_args = filter(lambda x: not issubclass_safe(x, PRIMITIVES), field_type.__args__)
                 for variant in field_args:
                     try:
                         decoded = cls._decode_field(field, variant, value)
@@ -472,7 +517,7 @@ class JsonSchemaMixin:
                         continue
                 if decoded is not None:
                     return decoded
-                if isinstance(value, primitives) and type(value) in field_type.__args__:
+                if isinstance(value, PRIMITIVES) and type(value) in field_type.__args__:
                     return cls._decode_field(field, type(value), value)
             elif field_type_name in MAPPING_TYPES:
                 def decoder(f, ft, val):
@@ -499,9 +544,9 @@ class JsonSchemaMixin:
                 def decoder(f, ft, val):
                     return cls._decode_field(f, ft.__supertype__, val)
             elif is_enum(field_type):
-                def decoder(_, ft, val): return ft(val)
+                decoder = _decoder_to_ft
             elif field_type == Any:
-                def decoder(_, __, val): return val
+                decoder = _decoder_identity
             if decoder is None:
                 warnings.warn(f"Unable to decode value for '{field}: {field_type_name}'")
                 return value
@@ -691,7 +736,7 @@ class JsonSchemaMixin:
                 field_schema, required = cls._get_field_schema(unwrap_final(field_type), schema_options)
             elif is_literal(field_type):
                 field_schema = {
-                    'enum': list(field_args if sys.version_info[:2] >= (3, 7) else field_type.__values__)
+                    'enum': list(field_args if IS_PYTHON_37_PLUS else field_type.__values__)
                 }
             elif is_enum(field_type):
                 member_types = set()
diff --git a/dataclasses_jsonschema/field_types.py b/dataclasses_jsonschema/field_types.py
index a7cfef2..f449d6a 100644
--- a/dataclasses_jsonschema/field_types.py
+++ b/dataclasses_jsonschema/field_types.py
@@ -3,9 +3,19 @@ from datetime import datetime, date
 from decimal import Decimal
 from ipaddress import IPv4Address, IPv6Address
 from typing import cast, TypeVar, Generic, Optional
-from uuid import UUID
 
-from dateutil.parser import parse
+# Note, iso8601 vs rfc3339 is subtle. rfc3339 is stricter, roughly a subset of iso8601.
+# ciso8601 doesn’t support the entirety of the ISO 8601 spec, only a popular subset.
+try:
+    from ciso8601 import parse_datetime
+except ImportError:
+    from dateutil.parser import parse
+    parse_datetime = parse  # type: ignore
+
+try:
+    from fastuuid import UUID
+except ImportError:
+    from uuid import UUID
 
 from .type_defs import JsonEncodable, JsonDict
 
@@ -17,10 +27,11 @@ class FieldEncoder(Generic[T, OutType]):
     """Base class for encoding fields to and from JSON encodable values"""
 
     def to_wire(self, value: T) -> OutType:
-        return cast(OutType, value)
+        # `cast` function call overhead adds up given how slow python is
+        return value  # type: ignore
 
     def to_python(self, value: OutType) -> T:
-        return cast(T, value)
+        return value  # type: ignore
 
     @property
     def json_schema(self) -> JsonDict:
@@ -28,13 +39,13 @@ class FieldEncoder(Generic[T, OutType]):
 
 
 class DateFieldEncoder(FieldEncoder[date, str]):
-    """Encodes dates to RFC3339 format"""
+    """Encodes dates to ISO8601 (compatible with RFC3339 subset) format"""
 
     def to_wire(self, value: date) -> str:
         return value.isoformat()
 
     def to_python(self, value: str) -> date:
-        return value if isinstance(value, date) else parse(cast(str, value)).date()
+        return value if isinstance(value, date) else parse_datetime(cast(str, value)).date()
 
     @property
     def json_schema(self) -> JsonDict:
@@ -42,7 +53,7 @@ class DateFieldEncoder(FieldEncoder[date, str]):
 
 
 class DateTimeFieldEncoder(FieldEncoder[datetime, str]):
-    """Encodes datetimes to RFC3339 format"""
+    """Encodes datetimes to ISO8601 (compatible with RFC3339 subset) format"""
 
     def to_wire(self, value: datetime) -> str:
         out = value.isoformat()
@@ -54,7 +65,7 @@ class DateTimeFieldEncoder(FieldEncoder[datetime, str]):
         return out
 
     def to_python(self, value: str) -> datetime:
-        return value if isinstance(value, datetime) else parse(cast(str, value))
+        return value if isinstance(value, datetime) else parse_datetime(cast(str, value))
 
     @property
     def json_schema(self) -> JsonDict:
@@ -75,7 +86,7 @@ class UuidField(FieldEncoder[UUID, str]):
         return UUID(value)
 
     @property
-    def json_schema(self):
+    def json_schema(self) -> JsonDict:
         return {
             'type': 'string',
             'format': 'uuid',
@@ -95,8 +106,8 @@ class DecimalField(FieldEncoder[Decimal, float]):
         return Decimal(str(value))
 
     @property
-    def json_schema(self):
-        schema = {'type': 'number'}
+    def json_schema(self) -> JsonDict:
+        schema: JsonDict = {'type': 'number'}
         if self.precision is not None and self.precision > 0:
             schema['multipleOf'] = float('0.' + '0' * (self.precision - 1) + '1')
         return schema
@@ -111,7 +122,7 @@ class IPv4AddressField(FieldEncoder[IPv4Address, str]):
         return IPv4Address(value)
 
     @property
-    def json_schema(self):
+    def json_schema(self) -> JsonDict:
         return {'type': 'string', 'format': 'ipv4'}
 
 
@@ -124,5 +135,5 @@ class IPv6AddressField(FieldEncoder[IPv6Address, str]):
         return IPv6Address(value)
 
     @property
-    def json_schema(self):
+    def json_schema(self) -> JsonDict:
         return {'type': 'string', 'format': 'ipv6'}
diff --git a/dataclasses_jsonschema/type_defs.py b/dataclasses_jsonschema/type_defs.py
index 2bf81dc..d90b520 100644
--- a/dataclasses_jsonschema/type_defs.py
+++ b/dataclasses_jsonschema/type_defs.py
@@ -55,3 +55,5 @@ NULL = _NULL_TYPE()
 
 T = TypeVar("T")
 Nullable = Union[T, _NULL_TYPE]
+
+NoneType = type(None)
diff --git a/setup.py b/setup.py
index 06cafc5..7b650ff 100644
--- a/setup.py
+++ b/setup.py
@@ -7,6 +7,18 @@ requires = [
     'dataclasses;python_version<"3.7"'
 ]
 
+test_dependencies = [
+    'apispec_webframeworks',
+    'apispec[yaml]',
+    'flake8',
+    'flask',
+    'mypy',
+    'pytest-ordering',
+    'pytest',
+    'tox-gh-actions',
+    'tox',
+    'types-python-dateutil',
+]
 
 def read(f):
     return open(f, encoding='utf-8').read()
@@ -23,20 +35,14 @@ setup(
     url='https://github.com/s-knibbs/dataclasses-jsonschema',
     install_requires=requires,
     extras_require={
+        'apispec': ["apispec"],
         'fast-validation': ["fastjsonschema"],
-        'apispec': ["apispec"]
+        'fast-dateparsing': ["ciso8601"],
+        'fast-uuid': ["fastuuid"],
+        'test': test_dependencies
     },
     setup_requires=['pytest-runner', 'setuptools_scm'],
-    tests_require=[
-        'pytest',
-        'flake8',
-        'mypy',
-        'apispec',
-        'apispec_webframeworks',
-        'flask',
-        'pytest-ordering',
-        'types-python-dateutil',
-    ],
+    tests_require=test_dependencies,
     license='MIT',
     use_scm_version=True,
     classifiers=[
@@ -47,6 +53,7 @@ setup(
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: 3.9',
+        'Programming Language :: Python :: 3.10',
         'Topic :: Software Development :: Libraries'
     ]
 )
diff --git a/tox.ini b/tox.ini
index d7d1af7..5a0b063 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,25 +1,24 @@
-# TODO: Figure out how to get tox to use setup.py deps
 [tox]
-envlist = py37,py38,py39,{py37,py38,py39}-fastvalidation
+envlist =
+    py{37,38,39,310}
+    py{37,38,39,310}-{fastvalidation,fastdateparsing,fastuuid,all}
 
 [gh-actions]
 python =
-    3.7: py37, py37-fastvalidation
-    3.8: py38, py38-fastvalidation
-    3.9: py39, py39-fastvalidation
+    3.7: py37
+    3.8: py38
+    3.9: py39
+    3.10: py310
 
+; To list all created environments + installation extras and more: `tox -l --showconfig`
 [testenv]
 commands =
     pytest tests
     flake8 dataclasses_jsonschema
     mypy dataclasses_jsonschema
-deps =
-    pytest
-    pytest-ordering
-    flake8
-    mypy
-    apispec[yaml]
-    apispec_webframeworks
-    flask
-    types-python-dateutil
-    fastvalidation: fastjsonschema
+extras =
+    test
+    fastvalidation: fast-validation
+    fastdateparsing: fast-dateparsing
+    fastuuid: fast-uuid
+    all: fast-validation, fast-dateparsing, fast-uuid


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 4dfd7b5..a0959a3 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -2,7 +2,11 @@ from typing import Optional, List, Dict, NewType, Any, Tuple, Union
 from dataclasses import dataclass, field
 from enum import Enum
 from datetime import datetime
-from uuid import UUID
+
+try:
+    from fastuuid import UUID
+except ImportError:
+    from uuid import UUID
 
 from dataclasses_jsonschema import JsonSchemaMixin, FieldEncoder
 
diff --git a/tests/test_core.py b/tests/test_core.py
index 38ef334..bceabb0 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -7,7 +7,11 @@ from dataclasses import dataclass, field
 from ipaddress import IPv4Address, IPv6Address
 from typing import List, NewType, Optional, Union, Set, Any, cast, Dict, Tuple
 from typing_extensions import Final, Literal
-from uuid import UUID
+
+try:
+    from fastuuid import UUID
+except ImportError:
+    from uuid import UUID
 
 from dataclasses_jsonschema.type_defs import Nullable, NULL, JsonDict
 from .conftest import Foo, Point, Recursive, OpaqueData, ShoppingCart, Product, ProductList, SubSchemas, Bar, Weekday, \