text: import datetime
  from _decimal import Decimal
  from enum import Enum
  
  from dataclasses import dataclass, field
  from ipaddress import IPv4Address, IPv6Address
  from typing import List, NewType, Optional, Union, Set, Any, cast
  from typing_extensions import Final, Literal
  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, \
      JsonSchemaMixin, Zoo, Baz
  import pytest
  
  from dataclasses_jsonschema import SchemaType, ValidationError, DecimalField, JsonSchemaMeta, FieldEncoder
  
  FOO_SCHEMA = {
      'description': 'A foo that foos',
      'properties': {
          'a': {'format': 'date-time', 'type': 'string'},
          'b': {'items': {'$ref': '#/definitions/Point'}, 'type': 'array'},
          'c': {'additionalProperties': {'type': 'integer'}, 'type': 'object'},
          'd': {'type': 'string', 'enum': ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']},
          'f': {'type': 'array', 'minItems': 2, 'maxItems': 2, 'items': [{'type': 'string'}, {'type': 'integer'}]},
          'g': {'type': 'array', 'items': {'type': 'string'}},
          'e': {'type': 'string', 'minLength': 5, 'maxLength': 8},
          'h': {'$ref': '#/definitions/Point'}
      },
      'type': 'object',
      'required': ['a', 'c', 'd', 'f', 'g']
  }
  
  SWAGGER_V2_FOO_SCHEMA = {
      'description': 'A foo that foos',
      'properties': {
          'a': {'format': 'date-time', 'type': 'string'},
          'b': {'items': {'$ref': '#/definitions/Point'}, 'type': 'array'},
          'c': {'additionalProperties': {'type': 'integer'}, 'type': 'object'},
          'd': {
              'type': 'string',
              'enum': ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
              'x-enum-name': 'Weekday'
          },
          'f': {'type': 'array', 'minItems': 2, 'maxItems': 2, 'items': [{'type': 'string'}, {'type': 'integer'}]},
          'g': {'type': 'array', 'items': {'type': 'string'}},
          'e': {'type': 'string', 'minLength': 5, 'maxLength': 8},
          'h': {'$ref': '#/definitions/Point'}
      },
      'type': 'object',
      'required': ['a', 'c', 'd', 'f', 'g']
  }
  
  SWAGGER_V3_FOO_SCHEMA = {
      'description': 'A foo that foos',
      'properties': {
          'a': {'format': 'date-time', 'type': 'string'},
          'b': {'items': {'$ref': '#/components/schemas/Point'}, 'type': 'array'},
          'c': {'additionalProperties': {'type': 'integer'}, 'type': 'object'},
          'd': {
              'type': 'string',
              'enum': ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
              'x-enum-name': 'Weekday'
          },
          'f': {'type': 'array', 'minItems': 2, 'maxItems': 2, 'items': [{'type': 'string'}, {'type': 'integer'}]},
          'g': {'type': 'array', 'items': {'type': 'string'}},
          'e': {'type': 'string', 'minLength': 5, 'maxLength': 8},
          'h': {'$ref': '#/components/schemas/Point'}
      },
      'type': 'object',
      'required': ['a', 'c', 'd', 'f', 'g']
  }
  
  # Fixme: Fields in description no longer match
  POINT_SCHEMA = {
      'description': Point.__doc__,
      'type': 'object',
      'properties': {
          'z': {'type': 'number', 'description': 'Point x coordinate'},
          'y': {'type': 'number', 'description': 'Point y coordinate'}
      },
      'required': ['z', 'y']
  }
  
  RECURSIVE_SCHEMA = {
      "description": Recursive.__doc__,
      "properties": {
          'a': {'type': 'string'},
          'b': {'$ref': '#/definitions/Recursive'}
      },
      'type': 'object',
      'required': ['a']
  }
  
  OPAQUE_DATA_SCHEMA = {
      "description": OpaqueData.__doc__,
      "properties": {
          'a': {'type': 'array'},
          'b': {'type': 'object'}
      },
      'type': 'object',
      'required': ['a', 'b']
  }
  
  PRODUCT_SCHEMA = {
      'description': Product.__doc__,
      'properties': {'cost': {'type': 'number', 'default': 20.0},
                     'name': {'type': 'string'}},
      'required': ['name'],
      'type': 'object'
  }
  
  SHOPPING_CART_SCHEMA = {
      'description': ShoppingCart.__doc__,
      'properties': {
          'items': {'items': {'$ref': '#/definitions/Product'}, 'type': 'array'},
      },
      'required': ['items'],
      'type': 'object'
  }
  PRODUCT_LIST_SCHEMA = {
      'description': ProductList.__doc__,
      'properties': {
          'products': {'additionalProperties': {'$ref': '#/definitions/Product'}, 'type': 'object'}
      },
      'type': 'object',
      'required': ['products']
  }
  BAR_SCHEMA = {
      'type': 'object',
      'description': "Type with union field",
      'properties': {
          'a': {
              'oneOf': [
                  {'type': 'string', 'enum': ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']},
                  {'$ref': '#/definitions/Point'}
              ]
          }
      },
      'required': ['a']
  }
  ZOO_SCHEMA = {
      'type': 'object',
      'description': "A zoo",
      'properties': {
          'animal_types': {'additionalProperties': {'type': 'string'}, 'type': 'object', 'default': {}}
      }
  }
  BAZ_SCHEMA = {
      'description': 'Type with nested default value',
      'properties': {'a': {'$ref': '#/definitions/Point', 'default': {'z': 0.0, 'y': 0.0}}},
      'type': 'object'
  }
  
  
  def compose_schema(schema, definitions=None):
      full_schema = {**schema, **{
          '$schema': 'http://json-schema.org/draft-06/schema#'
      }}
      if definitions is not None:
          full_schema['definitions'] = definitions
      return full_schema
  
  
  def test_field_with_default_factory():
      assert Zoo(animal_types={}) == Zoo.from_dict({})
      assert Zoo(animal_types={"snake": "reptile", "dog": "mammal"}) == Zoo.from_dict(
          {"animal_types": {"snake": "reptile", "dog": "mammal"}}
      )
  
  
  def test_field_with_default_dataclass():
      assert Baz(a=Point(0.0, 0.0)) == Baz.from_dict({})
  
  
  def test_embeddable_json_schema():
      expected = {'Point': POINT_SCHEMA, 'Foo': FOO_SCHEMA}
      assert expected == Foo.json_schema(embeddable=True)
      expected = {'Point': POINT_SCHEMA, 'Foo': SWAGGER_V2_FOO_SCHEMA}
      assert expected == SubSchemas.all_json_schemas(schema_type=SchemaType.SWAGGER_V2)
      expected = {'Point': POINT_SCHEMA, 'Foo': SWAGGER_V3_FOO_SCHEMA}
      assert expected == SubSchemas.all_json_schemas(schema_type=SchemaType.SWAGGER_V3)
      expected = {
          'Point': POINT_SCHEMA,
          'Foo': FOO_SCHEMA,
          'Recursive': RECURSIVE_SCHEMA,
          'Product': PRODUCT_SCHEMA,
          'ProductList': PRODUCT_LIST_SCHEMA,
          'Bar': BAR_SCHEMA,
          'ShoppingCart': SHOPPING_CART_SCHEMA,
          'OpaqueData': OPAQUE_DATA_SCHEMA,
          'Zoo': ZOO_SCHEMA,
          'Baz': BAZ_SCHEMA
      }
      assert expected == JsonSchemaMixin.all_json_schemas()
      with pytest.warns(DeprecationWarning):
          assert expected == JsonSchemaMixin.json_schema()
  
  
  def test_json_schema():
      definitions = {'Point': POINT_SCHEMA}
      assert compose_schema(FOO_SCHEMA, definitions) == Foo.json_schema()
  
  
  def test_serialise_deserialise():
      data = {
          'a': '2018-06-03T12:00:00+00:00',
          'b': [{'z': 1.2, 'y': 1.5}],
          'c': {'Mon': 1, 'Tue': 2},
          'd': 'Wednesday',
          'e': 'testing',
          'f': ['xyz', 6],
          'g': ['abc'],
          'h': {'z': 0.5, 'y': 1.0}
      }
      f = Foo.from_dict(data)
      assert f.f == ('xyz', 6)
      assert f.g == ('abc',)
      assert data == f.to_dict()
  
  
  def test_from_json_to_json():
      data = """
  {
    "a": "2018-06-03T12:00:00+00:00",
    "b": [
      {
        "z": 1.2,
        "y": 1.5
      }
    ],
    "c": {
      "Mon": 1,
      "Tue": 2
    },
    "d": "Wednesday",
    "f": [
      "xyz",
      6
    ],
    "g": [
      "abc"
    ],
    "e": "testing",
    "h": {
      "z": 0.5,
      "y": 1.0
    }
  }
  """
      f = Foo.from_json(data)
      assert f.f == ('xyz', 6)
      assert f.g == ('abc',)
      assert data.strip() == f.to_json(indent=2)
  
  
  def test_invalid_data():
      with pytest.raises(ValidationError):
          Point.from_dict({'z': 3.14, 'y': 'wrong'})
  
  
  def test_newtype_field_validation():
      with pytest.raises(ValidationError):
          Foo.from_dict({
              'a': '2018-06-03T12:00:00+00:00',
              'b': [{'z': 1.2, 'y': 1.5}],
              'c': {'Mon': 1, 'Tue': 2},
              'd': 'Wednesday',
              'e': 't',
              'f': ['xyz', 6],
              'g': ['abc']
          })
  
  
  def test_recursive_data():
      data = {"a": "test", "b": {"a": "test2"}}
      r = Recursive.from_dict(data)
      assert r.a == "test"
      assert r.to_dict() == data
  
  
  def test_recursive_validation():
      # a valid shopping cart containing two items
      data = {"items": [{"name": "apple", "cost": 0.4}, {"name": "banana", "cost": 0.6}]}
      cart = ShoppingCart.from_dict(data, validate=True)
      assert len(cart.items) == 2
      assert {item.name for item in cart.items} == {"apple", "banana"}
      assert cart.cost == 0.4 + 0.6
  
      # a shopping cart containing an invalid item
      data = {"items": [{"name": 123}]}
      with pytest.raises(ValidationError):
          ShoppingCart.from_dict(data, validate=True)
  
  
  def test_non_string_keys():
      p = ProductList(products={UUID('462b92e8-b3f7-4cb7-ae93-18e829c7e10d'): Product(name="hammer", cost=25.10)})
      expected_data = {"products": {"462b92e8-b3f7-4cb7-ae93-18e829c7e10d": {"name": "hammer", "cost": 25.10}}}
      assert p.to_dict() == expected_data
      assert ProductList.from_dict(expected_data) == p
  
  
  def test_type_union_schema():
      expected_schema = {
          **BAR_SCHEMA,
          'definitions': {'Point': POINT_SCHEMA},
          '$schema': 'http://json-schema.org/draft-06/schema#'
      }
      assert expected_schema == Bar.json_schema()
  
      # Should throw an error with SchemaType.SWAGGER_V2
      with pytest.raises(TypeError):
          Bar.json_schema(embeddable=True, schema_type=SchemaType.SWAGGER_V2)
  
  
  def test_type_union_serialise():
      assert Bar(a=Weekday.MON).to_dict() == {'a': 'Monday'}
      assert Bar(a=Point(x=1.25, y=3.5)).to_dict() == {'a': {'z': 1.25, 'y': 3.5}}
  
  
  def test_type_union_deserialise():
      assert Bar.from_dict({'a': 'Friday'}) == Bar(a=Weekday.FRI)
      assert Bar.from_dict({'a': {'z': 3.6, 'y': 10.1}}) == Bar(a=Point(x=3.6, y=10.1))
  
  
  def test_default_values():
      assert Product(name="hammer", cost=20.0) == Product.from_dict({"name": "hammer"})
  
  
  def test_default_factory():
      @dataclass
      class ClassTest(JsonSchemaMixin):
          attri: List[str] = field(default_factory=lambda: ['val'])
  
      assert 'required' not in ClassTest.json_schema().keys()
  
      assert ClassTest().attri == ['val']
      assert ClassTest().to_dict() == {'attri': ['val']}
      assert ClassTest.from_dict({}).attri == ['val']
  
  
  def test_read_only_field():
      @dataclass
      class Employee(JsonSchemaMixin):
          name: str
          department: str
          id: Optional[int] = field(metadata=JsonSchemaMeta(read_only=True), default=None)
  
      schema = Employee.json_schema(schema_type=SchemaType.OPENAPI_3, embeddable=True)
      assert schema['Employee']['properties']['id']['readOnly']
      assert 'readOnly' not in Employee.json_schema(schema_type=SchemaType.DRAFT_06)['properties']['id']
  
  
  def test_read_only_field_no_default():
      @dataclass
      class Employee(JsonSchemaMixin):
          name: str
          department: str
          id: int = field(metadata=JsonSchemaMeta(read_only=True))
  
      with pytest.warns(UserWarning):
          Employee.json_schema(schema_type=SchemaType.OPENAPI_3, embeddable=True)
  
  
  def test_field_types():
      Currency = NewType('Currency', Decimal)
      JsonSchemaMixin.register_field_encoders({
          Currency: DecimalField(precision=2)
      })
  
      @dataclass
      class AllFieldTypes(JsonSchemaMixin):
          """All field types used"""
          ip_address: IPv4Address
          ipv6_address: IPv6Address
          cost: Currency
          uuid: UUID
  
      expected_schema = compose_schema({
          'description': 'All field types used',
          'type': 'object',
          'properties': {
              'ip_address': {'type': 'string', 'format': 'ipv4'},
              'ipv6_address': {'type': 'string', 'format': 'ipv6'},
              'cost': {'type': 'number', 'multipleOf': 0.01},
              'uuid': {
                  'type': 'string',
                  'format': 'uuid',
                  'pattern': '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
              }
          },
          'required': ['ip_address', 'ipv6_address', 'cost', 'uuid']
      })
  
      assert AllFieldTypes.json_schema() == expected_schema
      expected_uuid = UUID('032bbcad-9ca2-4f36-9a63-43a036bc5755')
      expected_obj = AllFieldTypes(
          ip_address=IPv4Address('127.0.0.1'),
          ipv6_address=IPv6Address('::1'),
          cost=Currency(Decimal('49.99')),
          uuid=expected_uuid
      )
      expected_dict = {
          "ip_address": "127.0.0.1",
          "ipv6_address": "::1",
          "cost": 49.99,
          "uuid": str(expected_uuid)
      }
      assert expected_obj == AllFieldTypes.from_dict(expected_dict)
      assert expected_obj.to_dict() == expected_dict
  
  
  def test_field_metadata():
      @dataclass
      class Test(JsonSchemaMixin):
          """Dataclass with field metadata"""
          name: str = field(
              metadata=JsonSchemaMeta(
                  title="Title of the field",
                  description="Description of the field",
                  examples=["foo", "bar"],
                  extensions={
                      'field-group': 1
                  }
              )
          )
  
      expected_schema = {
          'type': 'object',
          'description': 'Dataclass with field metadata',
          'properties': {
              'name': {
                  'type': 'string',
                  'examples': ['foo', 'bar'],
                  'title': 'Title of the field',
                  'description': 'Description of the field'
              }
          },
          'required': ['name']
      }
      expected_full_schema = compose_schema(expected_schema)
      assert Test.json_schema() == expected_full_schema
      expected_schema['properties']['name']['x-field-group'] = 1
      assert Test.json_schema(schema_type=SchemaType.OPENAPI_3, embeddable=True) == {'Test': expected_schema}
      expected_schema['properties']['name']['example'] = 'foo'
      del expected_schema['properties']['name']['examples']
      assert Test.json_schema(schema_type=SchemaType.SWAGGER_V2, embeddable=True) == {'Test': expected_schema}
  
  
  def test_final_field():
  
      @dataclass
      class TestWithFinal(JsonSchemaMixin):
          """Dataclass with final field"""
          name: Final[str]
  
      expected_schema = {
          'type': 'object',
          'description': 'Dataclass with final field',
          'properties': {
              'name': {'type': 'string'}
          },
          'required': ['name']
      }
  
      assert TestWithFinal.json_schema() == compose_schema(expected_schema)
      assert TestWithFinal.from_dict({'name': 'foo'}) == TestWithFinal(name='foo')
      assert TestWithFinal(name='foo').to_dict() == {'name': 'foo'}
  
  
  def test_literal_types():
  
      @dataclass
      class ImageMeta(JsonSchemaMixin):
          """Image metadata"""
          bits_per_pixel: Literal[8, 16, 24, "true-color", None]
  
      expected_schema = {
          'type': 'object',
          'description': 'Image metadata',
          'properties': {
              'bits_per_pixel': {'enum': [8, 16, 24, 'true-color', None]}
          },
          'required': ['bits_per_pixel']
      }
      assert ImageMeta.json_schema() == compose_schema(expected_schema)
      assert ImageMeta(bits_per_pixel=16).to_dict() == {"bits_per_pixel": 16}
      assert ImageMeta.from_dict({"bits_per_pixel": 16}) == ImageMeta(bits_per_pixel=16)
  
  
  def test_from_object():
  
      class Genre(Enum):
          HORROR = "horror"
          BIOGRAPHY = "biography"
          THRILLER = "thriller"
  
      class AuthorModel:
  
          def __init__(self, name, age, books):
              self.name = name
              self.age = age
              self.books = books
  
      class BookModel:
  
          def __init__(self, name, first_print, publisher, genre):
              self.name = name
              self.first_print = first_print
              self.publisher = publisher
              self.genre = genre
  
      @dataclass
      class Book(JsonSchemaMixin):
          name: str
          publisher: str
          genre: Optional[Genre]
          first_print: Optional[datetime.datetime] = None
  
      @dataclass
      class Author(JsonSchemaMixin):
          name: str
          age: Optional[int] = None
          books: Optional[List[Book]] = None
  
      sample_author = AuthorModel(
          "Joe Bloggs", 32, [BookModel("Hello World!", datetime.datetime.utcnow(), "ACME Corp", "biography")]
      )
      expected_author = Author("Joe Bloggs", books=[Book("Hello World!", "ACME Corp", Genre.BIOGRAPHY)])
      assert Author.from_object(sample_author, exclude=('age', ('books', ('first_print',)))) == expected_author
  
      sample_author_2 = AuthorModel(
          "Joe Bloggs", 32, [BookModel("Hello World!", datetime.datetime.utcnow(), "ACME Corp", None)]
      )
      expected_author_2 = Author("Joe Bloggs", books=[Book("Hello World!", "ACME Corp", None)])
      assert Author.from_object(sample_author_2, exclude=('age', ('books', ('first_print',)))) == expected_author_2
  
      with pytest.raises(ValueError):
          Author.from_object(sample_author, exclude=('age', ('books', ('publisher',))))
  
  
  def test_serialise_deserialise_opaque_data():
      data = OpaqueData(a=["foo", 123], b={"foo": "bar", "baz": 123})
      dict_data = {"a": ["foo", 123], "b": {"foo": "bar", "baz": 123}}
      assert data.to_dict() == dict_data
      assert data == OpaqueData.from_dict(dict_data)
  
  
  def test_inherited_schema():
      @dataclass
      class Pet(JsonSchemaMixin):
          """A generic pet"""
          name: str
  
      @dataclass
      class Cat(Pet):
          """A cat"""
          hunting_skill: str
  
      @dataclass
      class Dog(Pet):
          """A dog"""
          breed: str
  
      expected_cat_schema = compose_schema({
          "description": "A cat",
          "allOf": [
              {"$ref": "#/definitions/Pet"},
              {
                  "type": "object",
                  "properties": {"hunting_skill": {"type": "string"}},
                  "required": ["hunting_skill"]
              },
          ]
      }, {
          "Pet": {
              "description": "A generic pet",
              "type": "object",
              "properties": {"name": {"type": "string"}},
              "required": ["name"]
          }
      })
      expected_dog_schema = compose_schema({
          "description": "A dog",
          "allOf": [
              {"$ref": "#/definitions/Pet"},
              {
                  "type": "object",
                  "properties": {"breed": {"type": "string"}},
                  "required": ["breed"]
              },
          ]
      }, {
          "Pet": {
              "description": "A generic pet",
              "type": "object",
              "properties": {"name": {"type": "string"}},
              "required": ["name"]
          }
      })
      assert Cat.json_schema() == expected_cat_schema
      assert Dog.json_schema() == expected_dog_schema
  
  
  def test_optional_union():
      @dataclass
      class Baz(JsonSchemaMixin):
          """Class with optional union"""
          a: Optional[Union[int, str]]
  
      expected_schema = compose_schema({
          "description": "Class with optional union",
          "type": "object",
          "properties": {
              "a": {"oneOf": [{"type": "integer"}, {"type": "string"}]}
          }
      })
      assert Baz.json_schema() == expected_schema
  
  
  def test_nullable_field():
      @dataclass
      class Employee(JsonSchemaMixin):
          """An employee"""
          name: str
          manager: Nullable[Optional[str]] = None
  
      expected_openapi_3_schema = {
          "type": "object",
          "description": "An employee",
          "properties": {
              "name": {"type": "string"},
              "manager": {"type": "string", "nullable": True}
          },
          "required": ["name"]
      }
      expected_json_schema = compose_schema({
          "type": "object",
          "description": "An employee",
          "properties": {
              "name": {"type": "string"},
              "manager": {"oneOf": [{"type": "string"}, {"type": "null"}]}
          },
          "required": ["name"]
      })
      assert Employee.json_schema() == expected_json_schema
      assert (
          Employee.json_schema(embeddable=True, schema_type=SchemaType.OPENAPI_3)["Employee"] == expected_openapi_3_schema
      )
  
      expected_obj = Employee(name="Joe Bloggs", manager=NULL)
      expected_dict = {"name": "Joe Bloggs", "manager": None}
      assert Employee.from_dict(expected_dict) == expected_obj
      assert expected_dict == expected_obj.to_dict()
  
  
  def test_underscore_fields():
      @dataclass
      class Album(JsonSchemaMixin):
          """An album"""
          _id: int
          name: str
  
      expected_json_schema = compose_schema({
          "type": "object",
          "description": "An album",
          "properties": {
              "name": {"type": "string"},
              "_id": {"type": "integer"}
          },
          "required": ["_id", "name"]
      })
      assert Album.json_schema() == expected_json_schema
      expected_data = {"name": "Foo", "_id": 5}
      expected_obj = Album(_id=5, name="Foo")
      assert expected_data == expected_obj.to_dict()
      assert expected_obj == Album.from_dict(expected_data)
  
  
  def test_discriminators():
      @dataclass
      class Pet(JsonSchemaMixin, discriminator=True):
          """A generic pet"""
          name: str
  
      @dataclass
      class Dog(Pet):
          """A dog"""
          breed: str
  
      expected_dog_schema = {
          "Dog": {
              "description": "A dog",
              "allOf": [
                  {"$ref": "#/components/schemas/Pet"},
                  {
                      "type": "object",
                      "properties": {"breed": {"type": "string"}},
                      "required": ["breed"]
                  },
              ]
          },
          "Pet": {
              "description": "A generic pet",
              "type": "object",
              "properties": {
                  "PetType": {"type": "string"},
                  "name": {"type": "string"}
              },
              "required": ["name", "PetType"],
              "discriminator": {"propertyName": "PetType"}
          }
      }
  
      assert Dog.json_schema(embeddable=True, schema_type=SchemaType.OPENAPI_3) == expected_dog_schema
      expected_dog = {
          "PetType": "Dog",
          "name": "Fido",
          "breed": "Dalmation"
      }
      assert Dog(name="Fido", breed="Dalmation").to_dict() == expected_dog
      assert Dog(name="Fido", breed="Dalmation") == Pet.from_dict(expected_dog)
  
  
  def test_set_decode_encode():
      @dataclass
      class BlogArticle(JsonSchemaMixin):
          """A blog article"""
          content: str
          tags: Set[str]
  
      expected_schema = compose_schema({
          "type": "object",
          "description": "A blog article",
          "properties": {
              "content": {"type": "string"},
              "tags": {"type": "array", "items": {"type": "string"}, "uniqueItems": True}
          },
          "required": ["content", "tags"]
      })
      assert expected_schema == BlogArticle.json_schema()
      expected_blog_dict = {
          "content": "Lorem ipsum dolor sit amet, consectetur adipiscing elit...",
          "tags": ["foo", "bar"]
      }
      expected_blog = BlogArticle(
          content="Lorem ipsum dolor sit amet, consectetur adipiscing elit...",
          tags={"foo", "bar"}
      )
      assert isinstance(expected_blog.to_dict()["tags"], list)
      assert len(expected_blog.to_dict()["tags"]) == 2
      assert BlogArticle.from_dict(expected_blog_dict) == expected_blog
  
  
  def test_any_type_schema():
      @dataclass
      class GraphNode(JsonSchemaMixin):
          """A graph node"""
          id: int
          data: Any
  
      expected_schema = compose_schema({
          "type": "object",
          "description": "A graph node",
          "properties": {
              "id": {"type": "integer"},
              "data": {}
          },
          "required": ["id", "data"]
      })
      assert GraphNode.json_schema() == expected_schema
  
  
  def test_additional_properties_allowed():
      @dataclass
      class Scorpion(JsonSchemaMixin, allow_additional_props=False):
          """A scorpion"""
          species: str
          venom_rating: int
  
      expected_schema = compose_schema({
          "type": "object",
          "description": "A scorpion",
          "properties": {
              "species": {"type": "string"},
              "venom_rating": {"type": "integer"},
          },
          "required": ["species", "venom_rating"],
          "additionalProperties": False,
      })
      assert Scorpion.json_schema() == expected_schema
  
  
  def test_property_serialisation():
      @dataclass
      class Rectangle(JsonSchemaMixin, serialise_properties=("area",)):
          """A rectangle"""
          width: float
          height: float
  
          @property
          def area(self) -> float:
              return self.width * self.height
  
          @property
          def perimeter(self) -> float:
              return 2 * self.width + 2 * self.height
  
      expected_schema = compose_schema({
          "type": "object",
          "description": "A rectangle",
          "properties": {
              "width": {"type": "number"},
              "height": {"type": "number"},
              "area": {"type": "number", "readOnly": True}
          },
          "required": ["width", "height"]
      })
      assert Rectangle.json_schema() == expected_schema
      rect = Rectangle(10, 20)
      assert rect.to_dict() == {"width": 10, "height": 20, "area": 200}
      assert Rectangle.from_dict({"width": 10, "height": 20}) == rect
  
  
  def test_property_serialisation_all_properties():
      @dataclass
      class Rectangle(JsonSchemaMixin, serialise_properties=True):
          """A rectangle"""
          width: float
          height: float
  
          @property
          def area(self) -> float:
              return self.width * self.height
  
          @property
          def perimeter(self) -> float:
              return 2 * self.width + 2 * self.height
  
      expected_schema = compose_schema({
          "type": "object",
          "description": "A rectangle",
          "properties": {
              "width": {"type": "number"},
              "height": {"type": "number"},
              "area": {"type": "number", "readOnly": True},
              "perimeter": {"type": "number", "readOnly": True}
          },
          "required": ["width", "height"]
      })
      assert Rectangle.json_schema() == expected_schema
  
  
  def test_inherited_field_narrowing():
      @dataclass
      class BaseObject(JsonSchemaMixin):
          """Base"""
          other: float
          field: str
  
      @dataclass
      class NarrowedObject(BaseObject, JsonSchemaMixin):
          """Narrowed"""
          field: Literal['staticstr']
  
      expected_schema = compose_schema({
        "allOf": [
          {
            "$ref": "#/definitions/BaseObject"
          },
          {
            "type": "object",
            "required": ["field"],
            "properties": {
              "field": {"enum": ["staticstr"]}
            }
          }
        ],
        "description": "Narrowed",
        "$schema": "http://json-schema.org/draft-06/schema#",
        "definitions": {
          "BaseObject": {
            "type": "object",
            "required": ["other", "field"],
            "properties": {
              "field": {"type": "string"},
              "other": {"type": "number"}
            },
            "description": "Base"
          }
        }
      })
  
      assert NarrowedObject.json_schema() == expected_schema
  
  
  def test_unrecognized_enum_value():
      class PetType(Enum):
          CAT = "cat"
          DOG = "dog"
  
      class FoodType(Enum):
          KIBBLE = "kibble"
          FRESH = "fresh"
  
      @dataclass
      class Pet(JsonSchemaMixin):
          name: str
          type: PetType
          favourite_food: Optional[FoodType] = None
  
      p = Pet.from_dict({'name': 'snakey', 'type': 'python', 'favourite_food': 'mice'}, validate_enums=False)
      assert p.type == "python"
      assert p.favourite_food == "mice"
  
      with pytest.warns(UserWarning):
          assert p.to_dict() == {'name': 'snakey', 'type': 'python', 'favourite_food': 'mice'}
  
  
  def test_inheritance_and_additional_properties_disallowed():
      @dataclass
      class Pet(JsonSchemaMixin):
          name: str
  
      # Currently this should raise an error until https://github.com/s-knibbs/dataclasses-jsonschema/issues/111
      # is implemented
      with pytest.raises(TypeError):
          @dataclass
          class Cat(Pet, allow_additional_props=False):
              hunting_skill: str
  
  
  def test_newtype_decoding():
      StrippedString = NewType('StrippedString', str)
  
      class StrippedStringField(FieldEncoder[StrippedString, str]):
  
          def to_python(self, value: str) -> StrippedString:
              return cast(StrippedString, value.strip())
  
          @property
          def json_schema(self) -> JsonDict:
              return {'type': 'string'}
  
      JsonSchemaMixin.register_field_encoders({StrippedString: StrippedStringField()})
  
      @dataclass
      class Pet(JsonSchemaMixin):
          name: StrippedString
          type: str
  
      p = Pet.from_dict({'name': '  Fido ', 'type': 'dog'})
      assert p.name == 'Fido'
  