Repository: s-knibbs/dataclasses-jsonschema (python)

Issue:
Implement python set serialisation
Serialisation / deserialisation of sets is currently not supported. Sets can also be indicated with the [uniqueItems property](https://json-schema.org/understanding-json-schema/reference/array.html?highlight=uniqueitems#uniqueness).

Pull request description:
Implement set support
Closes #95 

Patch:
diff --git a/dataclasses_jsonschema/__init__.py b/dataclasses_jsonschema/__init__.py
index a139823..8b79677 100644
--- a/dataclasses_jsonschema/__init__.py
+++ b/dataclasses_jsonschema/__init__.py
@@ -39,6 +39,11 @@ JSON_ENCODABLE_TYPES = {
     bool: {'type': 'boolean'},
     float: {'type': 'number'}
 }
+SEQUENCE_TYPES = {
+    'Sequence': list,
+    'List': list,
+    'Set': set
+}
 
 
 class ValidationError(Exception):
@@ -274,7 +279,7 @@ class JsonSchemaMixin:
                         cls._encode_field(ft.__args__[0], k, o): cls._encode_field(ft.__args__[1], v, o)
                         for k, v in val.items()
                     }
-            elif field_type_name in ('Sequence', 'List') or (field_type_name == "Tuple" and ... in field_type.__args__):
+            elif field_type_name in SEQUENCE_TYPES or (field_type_name == "Tuple" and ... in field_type.__args__):
                 def encoder(ft, val, o): return [cls._encode_field(ft.__args__[0], v, o) for v in val]
             elif field_type_name == 'Tuple':
                 def encoder(ft, val, o):
@@ -378,8 +383,8 @@ class JsonSchemaMixin:
                         cls._decode_field(f, ft.__args__[0], k): cls._decode_field(f, ft.__args__[1], v)
                         for k, v in val.items()
                     }
-            elif field_type_name in ('Sequence', 'List') or (field_type_name == "Tuple" and ... in field_type.__args__):
-                seq_type = tuple if field_type_name == "Tuple" else list
+            elif field_type_name in SEQUENCE_TYPES or (field_type_name == "Tuple" and ... in field_type.__args__):
+                seq_type = tuple if field_type_name == "Tuple" else SEQUENCE_TYPES[field_type_name]
 
                 def decoder(f, ft, val):
                     return seq_type(cls._decode_field(f, ft.__args__[0], v) for v in val)
@@ -587,10 +592,12 @@ class JsonSchemaMixin:
                     field_schema['additionalProperties'] = cls._get_field_schema(
                         field_type.__args__[1], schema_type
                     )[0]
-            elif field_type_name in ('Sequence', 'List') or (field_type_name == "Tuple" and ... in field_type.__args__):
+            elif field_type_name in SEQUENCE_TYPES or (field_type_name == "Tuple" and ... in field_type.__args__):
                 field_schema = {'type': 'array'}
                 if field_type.__args__[0] is not Any:
                     field_schema['items'] = cls._get_field_schema(field_type.__args__[0], schema_type)[0]
+                if field_type_name == "Set":
+                    field_schema['uniqueItems'] = True
             elif field_type_name == "Tuple":
                 tuple_len = len(field_type.__args__)
                 # TODO: How do we handle Optional type within lists / tuples


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index 51ac7df..dc7041b 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -4,7 +4,7 @@ from enum import Enum
 
 from dataclasses import dataclass, field
 from ipaddress import IPv4Address, IPv6Address
-from typing import List, NewType, Optional, Union
+from typing import List, NewType, Optional, Union, Set
 from typing_extensions import Final, Literal
 from uuid import UUID
 
@@ -717,3 +717,33 @@ def test_discriminators():
     }
     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