Repository: horejsek/python-fastjsonschema

Issue:
Incorrect validation of "uniqueItems"
While evaluating this library, I noticed that it's incorrectly validating `uniqueItems`. 

Minimal example:
```python
from fastjsonschema import compile

validate = compile({"type": "array", "uniqueItems": "true"})

def print_validity(data):
    try:
        validate(data)
        print("Equal and valid!")
    except Exception:
        print("Invalid")

d1 = {"foo": 1}
d1["bar"] = 2

d2 = {"bar": 2}
d2["foo"] = 1

assert d1 == d2

print_validity([1, 1])
print_validity([d1, d2])
```
This prints
```
Invalid
Equal and valid!
```

The issue is that `str` is used to be able to put `dict`s  in a `set` (as they are not otherwise hashable) in https://github.com/horejsek/python-fastjsonschema/blob/4b0d76cb07d78f047813c6bcda01879ee0a0216c/fastjsonschema/draft04.py#L353
but it's possible for `str(d1) != str(d2)` when `d1 == d2` as the example demonstrates.

Discussion:

Patch:
diff --git a/fastjsonschema/draft04.py b/fastjsonschema/draft04.py
index 5c40de7..b79b1f9 100644
--- a/fastjsonschema/draft04.py
+++ b/fastjsonschema/draft04.py
@@ -349,8 +349,16 @@ class CodeGeneratorDraft04(CodeGenerator):
         """
         self.create_variable_is_list()
         with self.l('if {variable}_is_list:'):
+            self.l(
+                'def fn(var): '
+                'return frozenset(dict((k, fn(v)) '
+                'for k, v in var.items()).items()) '
+                'if hasattr(var, "items") else tuple(fn(v) '
+                'for v in var) '
+                'if isinstance(var, (dict, list)) else str(var) '
+                'if isinstance(var, bool) else var')
             self.create_variable_with_length()
-            with self.l('if {variable}_len > len(set(str({variable}_x) for {variable}_x in {variable})):'):
+            with self.l('if {variable}_len > len(set(fn({variable}_x) for {variable}_x in {variable})):'):
                 self.exc('{name} must contain unique items', rule='uniqueItems')
 
     def generate_items(self):


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 756429e..de91132 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -13,6 +13,11 @@ from fastjsonschema import JsonSchemaException, compile
 from fastjsonschema.draft07 import CodeGeneratorDraft07
 
 
+def pytest_configure(config):
+    config.addinivalue_line("markers", "none")
+    config.addinivalue_line("markers", "benchmark")
+
+
 @pytest.fixture
 def asserter():
     def f(definition, value, expected, formats={}):
diff --git a/tests/test_array.py b/tests/test_array.py
index 3d77435..19289ab 100644
--- a/tests/test_array.py
+++ b/tests/test_array.py
@@ -51,6 +51,20 @@ def test_min_items(asserter, value, expected):
     ([1], [1]),
     ([1, 1], JsonSchemaException('data must contain unique items', value='{data}', name='data', definition='{definition}', rule='uniqueItems')),
     ([1, 2, 3], [1, 2, 3]),
+    ([True, False], [True, False]),
+    ([True, True], JsonSchemaException('data must contain unique items', value='{data}', name='data', definition='{definition}', rule='uniqueItems')),
+    (['abc', 'bce', 'hhh'], ['abc', 'bce', 'hhh']),
+    (['abc', 'abc'], JsonSchemaException('data must contain unique items', value='{data}', name='data', definition='{definition}', rule='uniqueItems')),
+    ([{'a': 'a'}, {'b': 'b'}], [{'a': 'a'}, {'b': 'b'}]),
+    ([{'a': 'a'}, {'a': 'a'}], JsonSchemaException('data must contain unique items', value='{data}', name='data', definition='{definition}', rule='uniqueItems')),
+    ([{'a': 'a', 'b': 'b'}, {'b': 'b', 'c': 'c'}], [{'a': 'a', 'b': 'b'}, {'b': 'b', 'c': 'c'}]),
+    ([{'a': 'a', 'b': 'b'}, {'b': 'b', 'a': 'a'}], JsonSchemaException('data must contain unique items', value='{data}', name='data', definition='{definition}', rule='uniqueItems')),
+    ([1, '1'], [1, '1']),
+    ([{'a': 'b'}, "{'a': 'b'}"], [{'a': 'b'}, "{'a': 'b'}"]),
+    ([[1, 2], [2, 1]], [[1, 2], [2, 1]]),
+    ([[1, 2], [1, 2]], JsonSchemaException('data must contain unique items', value='{data}', name='data', definition='{definition}', rule='uniqueItems')),
+    ([{'a': {'b': {'c': [1, 2]}}}, {'a': {'b': {'c': [1, 2]}}}], JsonSchemaException('data must contain unique items', value='{data}', name='data', definition='{definition}', rule='uniqueItems')),
+    ([{'a': {'b': {'c': [2, 1]}}}, {'a': {'b': {'c': [1, 2]}}}], [{'a': {'b': {'c': [2, 1]}}}, {'a': {'b': {'c': [1, 2]}}}]),
 ])
 def test_unique_items(asserter, value, expected):
     asserter({