Skip to content

Commit b983bb8

Browse files
Pass JSON document class through settings
This updates the JSON codec settings to include the document class to use during deserialization. This is primarily intended to allow customization of the discriminator.
1 parent f65db3c commit b983bb8

File tree

7 files changed

+71
-39
lines changed

7 files changed

+71
-39
lines changed

packages/smithy-json/src/smithy_json/__init__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
from smithy_core.serializers import ShapeSerializer
1010
from smithy_core.types import TimestampFormat
1111

12-
from ._private import JSONSettings as _JSONSettings
1312
from ._private.deserializers import JSONShapeDeserializer as _JSONShapeDeserializer
13+
from ._private.documents import JSONDocument
1414
from ._private.serializers import JSONShapeSerializer as _JSONShapeSerializer
15+
from .settings import JSONSettings
1516

1617
__version__: str = importlib.metadata.version("smithy-json")
18+
__all__ = ("JSONCodec", "JSONDocument", "JSONSettings")
1719

1820

1921
class JSONCodec(Codec):
@@ -25,6 +27,7 @@ def __init__(
2527
use_timestamp_format: bool = True,
2628
default_timestamp_format: TimestampFormat = TimestampFormat.DATE_TIME,
2729
default_namespace: str | None = None,
30+
document_class: type[JSONDocument] = JSONDocument,
2831
) -> None:
2932
"""Initializes a JSONCodec.
3033
@@ -34,11 +37,16 @@ def __init__(
3437
`smithy.api#timestampFormat` trait, if present.
3538
:param default_timestamp_format: The default timestamp format to use if the
3639
`smithy.api#timestampFormat` trait is not enabled or not present.
40+
:param default_namespace: The default namespace to use when determining a
41+
document's discriminator.
42+
:param document_class: The document class to deserialize to.
3743
"""
38-
self._settings = _JSONSettings(
44+
self._settings = JSONSettings(
3945
use_json_name=use_json_name,
4046
use_timestamp_format=use_timestamp_format,
4147
default_timestamp_format=default_timestamp_format,
48+
default_namespace=default_namespace,
49+
document_class=document_class,
4250
)
4351

4452
@property
Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,10 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0
3-
4-
from dataclasses import dataclass
53
from typing import Protocol, runtime_checkable
64

7-
from smithy_core.types import TimestampFormat
8-
95

106
@runtime_checkable
117
class Flushable(Protocol):
128
"""A protocol for objects that can be flushed."""
139

1410
def flush(self) -> None: ...
15-
16-
17-
@dataclass
18-
class JSONSettings:
19-
use_json_name: bool = True
20-
use_timestamp_format: bool = True
21-
default_timestamp_format: TimestampFormat = TimestampFormat.DATE_TIME

packages/smithy-json/src/smithy_json/_private/deserializers.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@
1818
from smithy_core.traits import JSONNameTrait, TimestampFormatTrait
1919
from smithy_core.types import TimestampFormat
2020

21-
from . import JSONSettings
22-
from .documents import JSONDocument
21+
from ..settings import JSONSettings
2322

2423
# TODO: put these type hints in a pyi somewhere. There here because ijson isn't
2524
# typed.
@@ -89,11 +88,9 @@ def peek(self) -> JSONParseEvent:
8988

9089

9190
class JSONShapeDeserializer(ShapeDeserializer):
92-
def __init__(
93-
self, source: BytesReader, *, settings: JSONSettings | None = None
94-
) -> None:
91+
def __init__(self, source: BytesReader, settings: JSONSettings) -> None:
9592
self._stream = BufferedParser(ijson.parse(source))
96-
self._settings = settings or JSONSettings()
93+
self._settings = settings
9794

9895
# A mapping of json name to member name for each shape. Since the deserializer
9996
# is shared and we don't know which shapes will be deserialized, this is
@@ -158,7 +155,9 @@ def read_string(self, schema: Schema) -> str:
158155
def read_document(self, schema: Schema) -> Document:
159156
start = next(self._stream)
160157
if start.type not in ("start_map", "start_array"):
161-
return JSONDocument(start.value, schema=schema, settings=self._settings)
158+
return self._settings.document_class(
159+
value=start.value, schema=schema, settings=self._settings
160+
)
162161

163162
end_type = "end_map" if start.type == "start_map" else "end_array"
164163
builder = cast(TypedObjectBuilder, ObjectBuilder())
@@ -168,7 +167,9 @@ def read_document(self, schema: Schema) -> Document:
168167
).path != start.path or event.type != end_type:
169168
builder.event(event.type, event.value)
170169

171-
return JSONDocument(builder.value, schema=schema, settings=self._settings)
170+
return self._settings.document_class(
171+
value=builder.value, schema=schema, settings=self._settings
172+
)
172173

173174
def read_timestamp(self, schema: Schema) -> datetime.datetime:
174175
format = self._settings.default_timestamp_format

packages/smithy-json/src/smithy_json/_private/documents.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,22 @@
1313
from smithy_core.traits import JSONNameTrait, TimestampFormatTrait
1414
from smithy_core.utils import expect_type
1515

16-
from . import JSONSettings
16+
from ..settings import JSONSettings
1717

1818

1919
class JSONDocument(Document):
20-
_schema: Schema
21-
_json_names: dict[str, str]
20+
_discriminator: ShapeID | None = None
2221

2322
def __init__(
2423
self,
2524
value: DocumentValue | dict[str, "Document"] | list["Document"],
25+
settings: JSONSettings | None = None,
2626
*,
2727
schema: Schema = DOCUMENT,
28-
settings: JSONSettings | None = None,
2928
) -> None:
3029
super().__init__(value, schema=schema)
31-
self._settings = settings or JSONSettings()
32-
self._json_names = {}
30+
self._settings = settings or JSONSettings(document_class=type(self))
31+
self._json_names: dict[str, str] = {}
3332

3433
if self._settings.use_json_name and schema.shape_type in (
3534
ShapeType.STRUCTURE,
@@ -39,12 +38,6 @@ def __init__(
3938
if json_name := member_schema.get_trait(JSONNameTrait):
4039
self._json_names[json_name.value] = member_name
4140

42-
@property
43-
def discriminator(self) -> ShapeID:
44-
if self._type is ShapeType.MAP:
45-
return ShapeID(self.as_map()["__type"].as_string())
46-
return super().discriminator
47-
4841
def as_blob(self) -> bytes:
4942
return b64decode(expect_type(str, self._value))
5043

packages/smithy-json/src/smithy_json/_private/serializers.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,17 @@
2121
from smithy_core.traits import JSONNameTrait, TimestampFormatTrait
2222
from smithy_core.types import TimestampFormat
2323

24-
from . import Flushable, JSONSettings
24+
from ..settings import JSONSettings
25+
from . import Flushable
2526

2627
_INF: float = float("inf")
2728
_NEG_INF: float = float("-inf")
2829

2930

3031
class JSONShapeSerializer(ShapeSerializer):
31-
def __init__(
32-
self, sink: BytesWriter, *, settings: JSONSettings | None = None
33-
) -> None:
32+
def __init__(self, sink: BytesWriter, settings: JSONSettings) -> None:
3433
self._stream = StreamingJSONEncoder(sink)
35-
self._settings = settings or JSONSettings()
34+
self._settings = settings
3635

3736
def begin_struct(
3837
self, schema: "Schema"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from dataclasses import dataclass
4+
from typing import TYPE_CHECKING
5+
6+
from smithy_core.types import TimestampFormat
7+
8+
if TYPE_CHECKING:
9+
from ._private.documents import JSONDocument
10+
11+
12+
@dataclass(slots=True)
13+
class JSONSettings:
14+
"""Settings for the JSON codec."""
15+
16+
document_class: type["JSONDocument"]
17+
"""The document class to deserialize to."""
18+
19+
use_json_name: bool = True
20+
"""Whether the codec should use `smithy.api#jsonName` trait, if present."""
21+
22+
use_timestamp_format: bool = True
23+
"""Whether the codec should use the `smithy.api#timestampFormat` trait, if
24+
present."""
25+
26+
default_timestamp_format: TimestampFormat = TimestampFormat.DATE_TIME
27+
"""The default timestamp format to use if the `smithy.api#timestampFormat` trait is
28+
not enabled or not present."""
29+
30+
default_namespace: str | None = None
31+
"""The default namespace to use when determining a document's discriminator."""

packages/smithy-json/tests/unit/test_deserializers.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
BIG_DECIMAL,
1010
BLOB,
1111
BOOLEAN,
12+
DOCUMENT,
1213
FLOAT,
1314
INTEGER,
1415
STRING,
1516
TIMESTAMP,
1617
)
17-
from smithy_json import JSONCodec
18+
from smithy_json import JSONCodec, JSONDocument
1819

1920
from . import (
2021
JSON_SERDE_CASES,
@@ -88,3 +89,13 @@ def _read_optional_map(k: str, d: ShapeDeserializer):
8889
assert actual_value == expected_value
8990
else:
9091
assert actual == expected
92+
93+
94+
class CustomDocument(JSONDocument):
95+
pass
96+
97+
98+
def test_uses_custom_document() -> None:
99+
codec = JSONCodec(document_class=CustomDocument)
100+
actual = codec.create_deserializer(b'{"foo": "bar"}').read_document(DOCUMENT)
101+
assert isinstance(actual, CustomDocument)

0 commit comments

Comments
 (0)