Repository: pydantic/pydantic

Issue:
Optional[conlist(..., unique_items=True)] raises exception if value is None
### Checks

* [x] I added a descriptive title to this issue
* [x] I have searched (google, github) for similar issues and couldn't find anything
* [x] I have read and followed [the docs](https://pydantic-docs.helpmanual.io/) and still think this is a bug

<!-- Sorry to sound so draconian, but every second saved replying to issues is time spend improving pydantic :-) -->

# Bug

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.9.0
            pydantic compiled: True
                 install path: /Users/.../code/venvs/.../lib/python3.9/site-packages/pydantic
               python version: 3.9.9 (v3.9.9:ccb0e6a345, Nov 15 2021, 13:06:05)  [Clang 13.0.0 (clang-1300.0.29.3)]
                     platform: macOS-12.0.1-arm64-arm-64bit
     optional deps. installed: ['dotenv', 'email-validator', 'typing-extensions']

```
<!-- or if you're using pydantic prior to v1.3, manually include: OS, python version and pydantic version -->

<!-- Please read the [docs](https://pydantic-docs.helpmanual.io/) and search through issues to
confirm your bug hasn't already been reported. -->

<!-- Where possible please include a self-contained code snippet describing your bug: -->

```py
from typing import Optional

from pydantic import BaseModel, conlist


class Model(BaseModel):
    fuel_types: Optional[conlist(str, min_items=1, unique_items=True)]


Model(fuel_types=None)

```
```
Traceback (most recent call last):
  File "/Users/.../code/python/.../ttt.py", line 10, in <module>
    Model(fuel_types=None)
  File "pydantic/main.py", line 331, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for Model
fuel_types
  'NoneType' object is not iterable (type=type_error)
```
This snippet above shouldn't lead to exception, but it does. Probably quick fix is replacing this code
https://github.com/samuelcolvin/pydantic/blob/91ecfd651e2f6f0820b5f12f79b9c33163d309d8/pydantic/types.py#L572-L578
with this one
```python
    @classmethod
    def unique_items_validator(cls, v: 'Optional[List[T]]') -> 'Optional[List[T]]':
        if v is None:
            return None

        for i, value in enumerate(v, start=1):
            if value in v[i:]:
                raise errors.ListUniqueItemsError()

        return v
```
Unexpected validation error with optional list field with unique_items check
### Checks

* [x] I added a descriptive title to this issue
* [x] I have searched (google, GitHub) for similar issues and couldn't find anything
* [x] I have read and followed [the docs](https://pydantic-docs.helpmanual.io/) and still think this is a bug


# Bug

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.9.0
            pydantic compiled: True
                 install path: /home/foo/lib/python3.8/site-packages/pydantic
               python version: 3.8.5 (default, Dec 29 2021, 04:21:09)  [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]
                     platform: Linux-3.10.0-1160.49.1.el7.x86_64-x86_64-with-glibc2.17
     optional deps. installed: ['dotenv', 'email-validator', 'typing-extensions']
```


Related discussion: https://github.com/samuelcolvin/pydantic/discussions/4050
```py
from typing import Optional, List
from pydantic import BaseModel, Field, conlist, ValidationError

class Model(BaseModel):
    foo: Optional[List[str]] = Field(None, unique_items=True)
    bar: conlist(str, unique_items=True) = Field(None)

try:
    Model(foo=None, bar=None)
except ValidationError as exc_info:
    assert exc_info.errors() == [
        {
            'loc': ('foo',), 
            'msg': "'NoneType' object is not iterable", 
            'type': 'type_error'
        }, 
        {
            'loc': ('bar',), 
            'msg': "'NoneType' object is not iterable", 
            'type': 'type_error'
        }
    ]
```

### Test case
``` py
def test_list_unique_items_with_optional():
    class Model(BaseModel):
        foo: Optional[List[str]] = Field(None, unique_items=True)
        bar: conlist(str, unique_items=True) = Field(None)

    assert Model().dict() == {'foo':None, 'bar': None}
    assert Model(foo=None, bar=None).dict() == {'foo':None, 'bar': None}
    assert Model(foo=["k1"], bar=["k1"]).dict() == {'foo': ["k1"], 'bar': ["k1"]}
    with pytest.raises(ValidationError) as exc_info:
        Model(foo=["k1", "k1"], bar=["k1", "k1"])
    assert exc_info.value.errors() == [
        {
            'loc': ('foo',),
            'msg': 'the list has duplicated items',
            'type': 'value_error.list.unique_items'
        },
        {
            'loc': ('bar',),
            'msg': 'the list has duplicated items',
            'type': 'value_error.list.unique_items'
        }
    ]
```
### Fix
Update the `unique_items_validator` function code to.
``` py
    @classmethod
    def unique_items_validator(cls, v: 'Optional[List[T]]') -> 'Optional[List[T]]':
        if v is None:
            return v
        for i, value in enumerate(v, start=1):
            if value in v[i:]:
                raise errors.ListUniqueItemsError()

        return v
```

`Tested this and ran all test cases and successfully passed.`

Please verify.
Cc: @samuelcolvin 


Discussion:

Shall I raise one PR to [1.9.X-fixes](https://github.com/samuelcolvin/pydantic/tree/1.9.X-fixes)
What happens on 1.8.2?
There is no support for `unique_items` in pydantic version 1.8.2
``` py
class Model(BaseModel):
    foo: Optional[List[str]] = Field(None, unique_items=True)

Model(foo=['a','a','a'], bar=None)
```
Won't raise any issues (duplicate items are there in the list) and 
``` py
class Model(BaseModel):
    bar: conlist(str, unique_items=True) = Field(None)

Model(bar=['a','a','a'])
```
Will raise an issue 
```
Traceback (most recent call last):
  File "/Users/developer/test.py", line 1, in <module>
    class Model(BaseModel):
  File "/Users/developer/test.py", line 2, in Model
    bar: conlist(str, unique_items=True) = Field(None)
  File "pydantic/types.py", line 509, in pydantic.types.conlist
TypeError: conlist() got an unexpected keyword argument 'unique_items'
```

ps: I need the support for `unique_items`.
Sorry, my bad. 🙈 

Yes, pr against that branch please.
Did this PR ever get made? If not, I'd be happy to create it.

Resolving patch:
diff --git a/pydantic/types.py b/pydantic/types.py
--- a/pydantic/types.py
+++ b/pydantic/types.py
@@ -599,7 +599,10 @@ def list_length_validator(cls, v: 'Optional[List[T]]') -> 'Optional[List[T]]':
         return v
 
     @classmethod
-    def unique_items_validator(cls, v: 'List[T]') -> 'List[T]':
+    def unique_items_validator(cls, v: 'Optional[List[T]]') -> 'Optional[List[T]]':
+        if v is None:
+            return None
+
         for i, value in enumerate(v, start=1):
             if value in v[i:]:
                 raise errors.ListUniqueItemsError()


Test patch:
diff --git a/tests/test_validators.py b/tests/test_validators.py
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -7,7 +7,7 @@
 import pytest
 from typing_extensions import Literal
 
-from pydantic import BaseModel, ConfigError, Extra, Field, ValidationError, errors, validator
+from pydantic import BaseModel, ConfigError, Extra, Field, ValidationError, conlist, errors, validator
 from pydantic.class_validators import make_generic_validator, root_validator
 
 
@@ -1329,3 +1329,19 @@ def post_root(cls, values):
 
     B(x='pika')
     assert validate_stub.call_args_list == [mocker.call('B', 'pre'), mocker.call('B', 'post')]
+
+
+def test_list_unique_items_with_optional():
+    class Model(BaseModel):
+        foo: Optional[List[str]] = Field(None, unique_items=True)
+        bar: conlist(str, unique_items=True) = Field(None)
+
+    assert Model().dict() == {'foo': None, 'bar': None}
+    assert Model(foo=None, bar=None).dict() == {'foo': None, 'bar': None}
+    assert Model(foo=['k1'], bar=['k1']).dict() == {'foo': ['k1'], 'bar': ['k1']}
+    with pytest.raises(ValidationError) as exc_info:
+        Model(foo=['k1', 'k1'], bar=['k1', 'k1'])
+    assert exc_info.value.errors() == [
+        {'loc': ('foo',), 'msg': 'the list has duplicated items', 'type': 'value_error.list.unique_items'},
+        {'loc': ('bar',), 'msg': 'the list has duplicated items', 'type': 'value_error.list.unique_items'},
+    ]