From 685f78441ae65415d44387b71f317da3a77c824d Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Mon, 21 Aug 2023 14:49:44 +0100 Subject: [PATCH] add test for str | UUID unions in JSON mode --- src/input/input_json.rs | 14 +++++++-- src/validators/uuid.rs | 10 +++--- tests/validators/test_union.py | 56 +++++++++++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/src/input/input_json.rs b/src/input/input_json.rs index 4a0b27b74..404d877be 100644 --- a/src/input/input_json.rs +++ b/src/input/input_json.rs @@ -83,13 +83,23 @@ impl<'a> Input<'a> for JsonInput { } } - fn validate_str(&'a self, _strict: bool) -> ValResult>> { + fn exact_str(&'a self) -> ValResult> { match self { - JsonInput::String(s) => Ok(ValidationMatch::exact(s.as_str().into())), + // Justification for `strict` instead of `exact` is that in JSON strings can also + // represent other datatypes such as UUID and date more exactly, so string is a + // converting input + JsonInput::String(s) => Ok(s.as_str().into()), _ => Err(ValError::new(ErrorTypeDefaults::StringType, self)), } } + fn validate_str(&'a self, _strict: bool) -> ValResult>> { + // Justification for `strict` instead of `exact` is that in JSON strings can also + // represent other datatypes such as UUID and date more exactly, so string is a + // converting input + self.exact_str().map(ValidationMatch::strict) + } + fn validate_bytes(&'a self, _strict: bool) -> ValResult> { match self { JsonInput::String(s) => Ok(s.as_bytes().into()), diff --git a/src/validators/uuid.rs b/src/validators/uuid.rs index 8fceb880f..458a0b159 100644 --- a/src/validators/uuid.rs +++ b/src/validators/uuid.rs @@ -114,11 +114,11 @@ impl Validator for UuidValidator { input, )) } else { - state.set_exactness_ceiling(if input.is_python() { - Exactness::Lax - } else { - Exactness::Strict - }); + // In python mode this is a coercion, in JSON mode we treat a UUID string as an + // exact match + if input.is_python() { + state.set_exactness_ceiling(Exactness::Lax); + } let uuid = self.get_uuid(input)?; self.create_py_uuid(py, class, &uuid) } diff --git a/tests/validators/test_union.py b/tests/validators/test_union.py index c570f9b6d..eac935c12 100644 --- a/tests/validators/test_union.py +++ b/tests/validators/test_union.py @@ -1,10 +1,13 @@ +from datetime import date, datetime, time from enum import Enum +from typing import Type, TypeVar from uuid import UUID import pytest from dirty_equals import IsFloat, IsInt +from pydantic.type_adapter import TypeAdapter -from pydantic_core import SchemaError, SchemaValidator, ValidationError, core_schema +from pydantic_core import MultiHostUrl, SchemaError, SchemaValidator, Url, ValidationError, core_schema from ..conftest import plain_repr @@ -511,3 +514,54 @@ def remove_prefix(v: str): '12345678-1234-5678-1234-567812345678' ) assert validator_called_count == 1 + + +T = TypeVar('T') + + +@pytest.mark.parametrize( + ('ty', 'input_value', 'expected_value'), + ( + (UUID, '12345678-1234-5678-1234-567812345678', UUID('12345678-1234-5678-1234-567812345678')), + pytest.param( + date, + '2020-01-01', + date(2020, 1, 1), + marks=pytest.mark.xfail(reason='remove set_exactness_unknown from date validator'), + ), + pytest.param( + time, + '00:00:00', + time(0, 0, 0), + marks=pytest.mark.xfail(reason='remove set_exactness_unknown from time validator'), + ), + pytest.param( + datetime, + '2020-01-01:00:00:00', + datetime(2020, 1, 1, 0, 0, 0), + marks=pytest.mark.xfail(reason='remove set_exactness_unknown from datetime validator'), + ), + pytest.param( + Url, + 'https://foo.com', + Url('https://foo.com'), + marks=pytest.mark.xfail(reason='remove set_exactness_unknown from url validator'), + ), + pytest.param( + MultiHostUrl, + 'https://bar.com,foo.com', + MultiHostUrl('https://bar.com,foo.com'), + marks=pytest.mark.xfail(reason='remove set_exactness_unknown from multihosturl validator'), + ), + ), +) +def test_smart_union_json_string_types(ty: Type[T], input_value: str, expected_value: T): + # Many types have to be represented in strings as JSON, we make sure that + # when parsing in JSON mode these types are preferred + assert TypeAdapter(ty | str).validate_json(f'"{input_value}"') == expected_value + # in Python mode the string will be preferred + assert TypeAdapter(ty | str).validate_python(input_value) == input_value + + # Repeat with reversed order + assert TypeAdapter(str | ty).validate_json(f'"{input_value}"') == expected_value + assert TypeAdapter(str | ty).validate_python(input_value) == input_value