Repository: pydantic/pydantic

Issue:
No validation for UUIDs
There is not validator for UUIDs.
Can we add one?


Discussion:
Yes, should be very simple. I meant to but hadn't got round to it.

Should be as simple as adding a method [here](https://github.com/samuelcolvin/pydantic/blob/master/pydantic/validators.py#L92). Want to submit a PR?
I'll add it to my todo list :)
I tried another solution because I wanted to use strings (input & output). see https://github.com/samuelcolvin/pydantic/compare/master...mgu:add-type-uuidstr
But this also could be done by adding a validator for `uuid.UUID` in `validators.py`.

I don't know what is the best solution

Resolving patch:
diff --git a/docs/examples/exotic.py b/docs/examples/exotic.py
--- a/docs/examples/exotic.py
+++ b/docs/examples/exotic.py
@@ -1,6 +1,8 @@
 from pathlib import Path
+from uuid import UUID
 
-from pydantic import DSN, BaseModel, EmailStr, NameEmail, PyObject, conint, constr, PositiveInt, NegativeInt
+from pydantic import (DSN, BaseModel, EmailStr, NameEmail, PyObject, conint,
+                      constr, PositiveInt, NegativeInt)
 
 
 class Model(BaseModel):
@@ -25,6 +27,7 @@ class Model(BaseModel):
     db_driver = 'postgres'
     db_query: dict = None
     dsn: DSN = None
+    uuid: UUID = None
 
 m = Model(
     cos_function='math.cos',
@@ -36,6 +39,7 @@ class Model(BaseModel):
     neg_int=-1,
     email_address='Samuel Colvin <s@muelcolvin.com >',
     email_and_name='Samuel Colvin <s@muelcolvin.com >',
+    uuid='ebcdab58-6eb8-46fb-a190-d07a33e9eac8'
 )
 print(m.values())
 """
@@ -49,6 +53,7 @@ class Model(BaseModel):
     'email_address': 's@muelcolvin.com',
     'email_and_name': <NameEmail("Samuel Colvin <s@muelcolvin.com>")>,
     ...
-    'dsn': 'postgres://postgres@localhost:5432/foobar'
+    'dsn': 'postgres://postgres@localhost:5432/foobar',
+    ' uuid': UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8'),
 }
 """
diff --git a/pydantic/validators.py b/pydantic/validators.py
--- a/pydantic/validators.py
+++ b/pydantic/validators.py
@@ -4,6 +4,7 @@
 from enum import Enum
 from pathlib import Path
 from typing import Any
+from uuid import UUID
 
 from .datetime_parse import parse_date, parse_datetime, parse_duration, parse_time
 from .exceptions import ConfigError
@@ -26,7 +27,7 @@ def str_validator(v) -> str:
         # is there anything else we want to add here? If you think so, create an issue.
         return str(v)
     else:
-        raise ValueError(f'str or byte type expected not {type(v)}')
+        raise TypeError(f'str or byte type expected not {type(v)}')
 
 
 def bytes_validator(v) -> bytes:
@@ -99,6 +100,17 @@ def enum_validator(v, field, **kwargs) -> Enum:
     return field.type_(v)
 
 
+def uuid_validator(v) -> UUID:
+    if isinstance(v, UUID):
+        return v
+    elif isinstance(v, str):
+        return UUID(v)
+    elif isinstance(v, (bytes, bytearray)):
+        return UUID(v.decode())
+    else:
+        raise ValueError(f'str, byte or native UUID type expected not {type(v)}')
+
+
 # order is important here, for example: bool is a subclass of int so has to come first, datetime before date same
 _VALIDATORS = [
     (Enum, [enum_validator]),
@@ -122,6 +134,7 @@ def enum_validator(v, field, **kwargs) -> Enum:
     (list, [list_validator]),
     (tuple, [tuple_validator]),
     (set, [set_validator]),
+    (UUID, [not_none_validator, uuid_validator]),
 ]
 
 
diff --git a/pydantic/version.py b/pydantic/version.py
--- a/pydantic/version.py
+++ b/pydantic/version.py
@@ -2,4 +2,4 @@
 
 __all__ = ['VERSION']
 
-VERSION = StrictVersion('0.4.0')
+VERSION = StrictVersion('0.5.0')


Test patch:
diff --git a/tests/test_types.py b/tests/test_types.py
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -2,6 +2,7 @@
 from collections import OrderedDict
 from datetime import date, datetime, time, timedelta
 from enum import Enum, IntEnum
+from uuid import UUID
 
 import pytest
 
@@ -87,6 +88,7 @@ class CheckModel(BaseModel):
     bytes_check = b's'
     int_check = 1
     float_check = 1.0
+    uuid_check: UUID = UUID('7bd00d58-6485-4ca6-b889-3da6d8df3ee4')
 
     class Config:
         max_anystr_length = 10
@@ -141,6 +143,12 @@ class Config:
     ('float_check', 123, ValidationError),
     ('float_check', '123', ValidationError),
     ('float_check', b'123', ValidationError),
+
+    ('uuid_check', 'ebcdab58-6eb8-46fb-a190-d07a33e9eac8', UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8')),
+    ('uuid_check', UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8'), UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8')),
+    ('uuid_check', b'ebcdab58-6eb8-46fb-a190-d07a33e9eac8', UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8')),
+    ('uuid_check', 'ebcdab58-6eb8-46fb-a190-', ValidationError),
+    ('uuid_check', 123, ValidationError),
 ])
 def test_default_validators(field, value, result):
     kwargs = {field: value}
@@ -396,3 +404,18 @@ class Model(BaseModel):
 
     with pytest.raises(ValidationError):
         Model(v=b'foobar')
+
+
+def test_uuid_error():
+    class Model(BaseModel):
+        v: UUID
+
+    with pytest.raises(ValidationError) as exc_info:
+        Model(v='ebcdab58-6eb8-46fb-a190-d07a3')
+    assert """\
+error validating input
+v:
+  badly formed hexadecimal UUID string (error_type=ValueError track=UUID)""" == str(exc_info.value)
+
+    with pytest.raises(ValidationError):
+        Model(v=None)