Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -358,9 +358,17 @@ def _assign_field_values(self, item: Parsable) -> None:
deserialize but the model doesn't support additional data"
)

def __is_four_digit_number(self, value: str) -> bool:
pattern = r'^\d{4}$'
return bool(re.match(pattern, value))
# Anchored shapes that are unambiguous date/datetime/UUID literals. Untyped
# strings are only converted when they match one of these; anything else is
# returned as-is. Lenient guessing (time, timedelta, compact/partial dates)
# mangled real-world text values such as "19-2026" (parsed as a time with a
# UTC offset), "PT" (empty duration) or "11.0" (11:00). This mirrors the
# .NET JsonParseNode, which only attempts strict DateTime/DateTimeOffset/
# Guid parses on strings.
_ISO_DATETIME_OR_DATE_PATTERN = re.compile(r'^\d{4}-\d{2}-\d{2}([Tt ]\d{2}:\d{2}|$)')
_CANONICAL_UUID_PATTERN = re.compile(
r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
)

def try_get_anything(self, value: Any) -> Any:
if isinstance(value, (int, float, bool)) or value is None:
Expand All @@ -370,31 +378,16 @@ def try_get_anything(self, value: Any) -> Any:
if isinstance(value, dict):
return dict(map(lambda x: (x[0], self.try_get_anything(x[1])), value.items()))
if isinstance(value, str):
try:
if self.__is_four_digit_number(value):
return value
if value.isdigit():
return value
datetime_obj = datetime_from_iso_format_compat(value)
return datetime_obj
except ValueError:
pass
try:
return UUID(value)
except:
pass
try:
return parse_timedelta_string(value)
except ValueError:
pass
try:
return date.fromisoformat(value)
except ValueError:
pass
try:
return time_from_iso_format_compat(value)
except ValueError:
pass
if self._ISO_DATETIME_OR_DATE_PATTERN.match(value):
try:
return datetime_from_iso_format_compat(value)
except ValueError:
pass
elif self._CANONICAL_UUID_PATTERN.match(value):
try:
return UUID(value)
except ValueError:
pass
return value
raise ValueError(f"Unexpected additional value type {type(value)} during deserialization.")

Expand Down
41 changes: 41 additions & 0 deletions packages/serialization/json/tests/unit/test_json_parse_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,47 @@ def test_get_anythin_does_convert_date_string_to_datetime():
assert result == datetime(2023, 10, 5, 14, 48, tzinfo=timezone.utc)


@pytest.mark.parametrize(
"value",
[
# microsoftgraph/msgraph-sdk-python#1340: text values coerced to
# datetime.time via lenient time.fromisoformat (3.11+ accepts an
# hour plus a UTC-offset-looking suffix or a fraction separator)
"19-2026", # invoice number, parsed as time(19, 0) at offset -20:26
"100-012863299", # invoice number
"23.085", # parsed as time(23, 0, 0, 85000)
"11.0", # version string, parsed as time(11, 0)
# coerced to timedelta via parse_timedelta_string
"PT", # country code, parsed as an empty ISO 8601 duration
"10:30", # parsed as hh:mm
# microsoft/kiota-python#487: 32-hex string coerced to UUID
"d41d8cd98f00b204e9800998ecf8427e",
# date-like fragments must not be parsed as dates
"12-25",
"2023-10",
],
)
def test_get_anything_does_not_coerce_text_values(value):
parse_node = JsonParseNode(value)
result = parse_node.try_get_anything(value)
assert isinstance(result, str)
assert result == value


def test_get_anything_converts_date_only_string_to_datetime():
parse_node = JsonParseNode("2023-10-05")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have variations with time, and time + offset to prevent any further regression?

result = parse_node.try_get_anything("2023-10-05")
assert isinstance(result, datetime)
assert result == datetime(2023, 10, 5, 0, 0)


def test_get_anything_converts_canonical_uuid_string():
parse_node = JsonParseNode("8f841f30-e6e3-439a-a812-ebd369559c36")
result = parse_node.try_get_anything("8f841f30-e6e3-439a-a812-ebd369559c36")
assert isinstance(result, UUID)
assert result == UUID("8f841f30-e6e3-439a-a812-ebd369559c36")


def test_get_object_value(user1_json):
parse_node = JsonParseNode(json.loads(user1_json))
result = parse_node.get_object_value(User)
Expand Down