From 91da3b792502a4f423019db9e50e58edf0d0ca9f 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] uuid test - broken --- src/input/input_json.rs | 14 ++++++++++++-- src/validators/uuid.rs | 10 +++++----- tests/validators/test_union.py | 25 +++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 7 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..0a418e04d 100644 --- a/tests/validators/test_union.py +++ b/tests/validators/test_union.py @@ -1,8 +1,11 @@ +from datetime import date 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 @@ -511,3 +514,25 @@ 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')), + (date, '2020-01-01', date(2020, 1, 1)), + ), +) +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