Skip to content

Commit 7e6ac05

Browse files
Prefer AnyComponent type over SerializeAsAny to prevent loss of data when deserializing.
1 parent 5a4a970 commit 7e6ac05

2 files changed

Lines changed: 55 additions & 30 deletions

File tree

src/oshconnect/schema_datamodels.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from .csapi4py.constants import ObservationFormat
1616
from .encoding import Encoding
1717
from .geometry import Geometry
18-
from .swe_components import AnyComponentSchema
18+
from .swe_components import AnyComponent
1919

2020
"""
2121
In many of the top level resource models there is a "schema" field of some description. These models are meant to ease
@@ -51,7 +51,7 @@ class SWEJSONCommandSchema(CommandSchema):
5151

5252
command_format: str = Field("application/swe+json", alias='commandFormat')
5353
encoding: SerializeAsAny[Encoding] = Field(...)
54-
record_schema: SerializeAsAny[AnyComponentSchema] = Field(..., alias='recordSchema')
54+
record_schema: AnyComponent = Field(..., alias='recordSchema')
5555

5656

5757
class JSONCommandSchema(CommandSchema):
@@ -61,9 +61,9 @@ class JSONCommandSchema(CommandSchema):
6161
model_config = ConfigDict(populate_by_name=True)
6262

6363
command_format: str = Field("application/json", alias='commandFormat')
64-
params_schema: SerializeAsAny[AnyComponentSchema] = Field(..., alias='parametersSchema')
65-
result_schema: SerializeAsAny[AnyComponentSchema] = Field(None, alias='resultSchema')
66-
feasibility_schema: SerializeAsAny[AnyComponentSchema] = Field(None, alias='feasibilityResultSchema')
64+
params_schema: AnyComponent = Field(..., alias='parametersSchema')
65+
result_schema: AnyComponent = Field(None, alias='resultSchema')
66+
feasibility_schema: AnyComponent = Field(None, alias='feasibilityResultSchema')
6767

6868

6969
class DatastreamRecordSchema(BaseModel):
@@ -75,10 +75,14 @@ class DatastreamRecordSchema(BaseModel):
7575
obs_format: str = Field(..., alias='obsFormat')
7676

7777

78+
# `encoding` is required per CS API Part 2 §16.2.3 Requirement 109.B, but the
79+
# OSH server omits it from /datastreams/{id}/schema responses. We accept it as
80+
# optional to be able to parse what the server returns. See
81+
# docs/osh_spec_deviations.md (swe-json-missing-encoding).
7882
class SWEDatastreamRecordSchema(DatastreamRecordSchema):
7983
model_config = ConfigDict(populate_by_name=True)
80-
encoding: SerializeAsAny[Encoding] = Field(...)
81-
record_schema: SerializeAsAny[AnyComponentSchema] = Field(..., alias='recordSchema')
84+
encoding: SerializeAsAny[Encoding] = Field(None)
85+
record_schema: AnyComponent = Field(..., alias='recordSchema')
8286

8387
@field_validator('obs_format')
8488
@classmethod
@@ -100,8 +104,8 @@ class JSONDatastreamRecordSchema(DatastreamRecordSchema):
100104
model_config = ConfigDict(populate_by_name=True)
101105

102106
obs_format: str = Field(ObservationFormat.JSON.value, alias='obsFormat')
103-
result_schema: SerializeAsAny[AnyComponentSchema] = Field(None, alias='resultSchema')
104-
parameters_schema: SerializeAsAny[AnyComponentSchema] = Field(None, alias='parametersSchema')
107+
result_schema: AnyComponent = Field(None, alias='resultSchema')
108+
parameters_schema: AnyComponent = Field(None, alias='parametersSchema')
105109
result_link: dict = Field(None, alias='resultLink')
106110

107111
@field_validator('obs_format')

src/oshconnect/swe_components.py

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
from __future__ import annotations
99

1010
from numbers import Real
11-
from typing import Union, Any, Literal
11+
from typing import Union, Any, Literal, Annotated
1212

13-
from pydantic import BaseModel, Field, field_validator, SerializeAsAny
13+
from pydantic import BaseModel, ConfigDict, Field, field_validator, SerializeAsAny
1414

1515
from .csapi4py.constants import GeometryTypes
1616
from .api_utils import UCUMCode, URI
@@ -31,6 +31,7 @@
3131

3232

3333
class AnyComponentSchema(BaseModel):
34+
model_config = ConfigDict(populate_by_name=True)
3435
type: str = Field(...)
3536
id: str = Field(None)
3637
label: str = Field(None)
@@ -42,33 +43,39 @@ class AnyComponentSchema(BaseModel):
4243

4344
class DataRecordSchema(AnyComponentSchema):
4445
type: Literal["DataRecord"] = "DataRecord"
45-
fields: SerializeAsAny[list[AnyComponentSchema]] = Field(...)
46+
# `name` is not part of AbstractDataComponent in SWE Common 3 — it belongs to
47+
# the SoftNamedProperty wrapper that binds a component as a record field. We
48+
# accept it here only because the OSH server emits `name` at the root-level
49+
# DataRecord of a datastream's recordSchema/resultSchema. See
50+
# docs/osh_spec_deviations.md (root-component-name).
51+
name: str = Field(None)
52+
fields: list["AnyComponent"] = Field(...)
4653

4754

4855
class VectorSchema(AnyComponentSchema):
4956
label: str = Field(...)
5057
name: str = Field(...)
5158
type: Literal["Vector"] = "Vector"
5259
definition: str = Field(...)
53-
reference_frame: str = Field(..., serialization_alias='referenceFrame')
54-
local_frame: str = Field(None, serialization_alias='localFrame')
60+
reference_frame: str = Field(..., alias='referenceFrame')
61+
local_frame: str = Field(None, alias='localFrame')
5562
# TODO: VERIFY might need to be moved further down when these are defined
5663
coordinates: SerializeAsAny[Union[list[CountSchema], list[QuantitySchema], list[TimeSchema]]] = Field(...)
5764

5865

5966
class DataArraySchema(AnyComponentSchema):
6067
type: Literal["DataArray"] = "DataArray"
6168
name: str = Field(...)
62-
element_count: dict | str | CountSchema = Field(..., serialization_alias='elementCount') # Should type of Count
63-
element_type: SerializeAsAny[AnyComponentSchema] = Field(..., serialization_alias='elementType')
69+
element_count: dict | str | CountSchema = Field(..., alias='elementCount') # Should type of Count
70+
element_type: "AnyComponent" = Field(..., alias='elementType')
6471
encoding: str = Field(...) # TODO: implement an encodings class
6572
values: list = Field(None)
6673

6774

6875
class MatrixSchema(AnyComponentSchema):
6976
type: Literal["Matrix"] = "Matrix"
70-
element_count: dict | str | CountSchema = Field(..., serialization_alias='elementCount') # Should be type of Count
71-
element_type: SerializeAsAny[list[AnyComponentSchema]] = Field(..., serialization_alias='elementType')
77+
element_count: dict | str | CountSchema = Field(..., alias='elementCount') # Should be type of Count
78+
element_type: list["AnyComponent"] = Field(..., alias='elementType')
7279
encoding: str = Field(...) # TODO: implement an encodings class
7380
values: list = Field(None)
7481
reference_frame: str = Field(None)
@@ -79,8 +86,8 @@ class DataChoiceSchema(AnyComponentSchema):
7986
type: Literal["DataChoice"] = "DataChoice"
8087
updatable: bool = Field(False)
8188
optional: bool = Field(False)
82-
choice_value: CategorySchema = Field(..., serialization_alias='choiceValue') # TODO: Might be called "choiceValues"
83-
items: SerializeAsAny[list[AnyComponentSchema]] = Field(...)
89+
choice_value: CategorySchema = Field(..., alias='choiceValue') # TODO: Might be called "choiceValues"
90+
items: list["AnyComponent"] = Field(...)
8491

8592

8693
class GeometrySchema(AnyComponentSchema):
@@ -99,7 +106,7 @@ class GeometrySchema(AnyComponentSchema):
99106
GeometryTypes.MULTI_POLYGON.value
100107
]
101108
})
102-
nil_values: list = Field(None, serialization_alias='nilValues')
109+
nil_values: list = Field(None, alias='nilValues')
103110
srs: str = Field(...)
104111
value: Geometry = Field(None)
105112

@@ -111,11 +118,13 @@ class AnySimpleComponentSchema(AnyComponentSchema):
111118
updatable: bool = Field(False)
112119
optional: bool = Field(False)
113120
definition: str = Field(...)
114-
reference_frame: str = Field(None, serialization_alias='referenceFrame')
115-
axis_id: str = Field(None, serialization_alias='axisID')
116-
quality: list[Union[QuantitySchema, QuantityRangeSchema, CategorySchema, TextSchema]] = Field(
117-
None) # TODO: Union[Quantity, QuantityRange, Category, Text]
118-
nil_values: list = Field(None, serialization_alias='nilValues')
121+
reference_frame: str = Field(None, alias='referenceFrame')
122+
axis_id: str = Field(None, alias='axisID')
123+
quality: list[Annotated[
124+
Union[QuantitySchema, QuantityRangeSchema, CategorySchema, TextSchema],
125+
Field(discriminator='type'),
126+
]] = Field(None)
127+
nil_values: list = Field(None, alias='nilValues')
119128
constraint: Any = Field(None)
120129
value: Any = Field(None)
121130
name: str = Field(...)
@@ -165,15 +174,15 @@ def validate_value(cls, v):
165174
class TimeSchema(AnyScalarComponentSchema):
166175
type: Literal["Time"] = "Time"
167176
value: str = Field(None)
168-
reference_time: str = Field(None, serialization_alias='referenceTime')
177+
reference_time: str = Field(None, alias='referenceTime')
169178
local_frame: str = Field(None)
170179
uom: Union[UCUMCode, URI] = Field(...)
171180

172181

173182
class CategorySchema(AnyScalarComponentSchema):
174183
type: Literal["Category"] = "Category"
175184
value: str = Field(None)
176-
code_space: str = Field(None, serialization_alias='codeSpace')
185+
code_space: str = Field(None, alias='codeSpace')
177186

178187

179188
class TextSchema(AnyScalarComponentSchema):
@@ -196,12 +205,24 @@ class QuantityRangeSchema(AnySimpleComponentSchema):
196205
class TimeRangeSchema(AnySimpleComponentSchema):
197206
type: Literal["TimeRange"] = "TimeRange"
198207
value: list[str] = Field(None)
199-
reference_time: str = Field(None, serialization_alias='referenceTime')
208+
reference_time: str = Field(None, alias='referenceTime')
200209
local_frame: str = Field(None)
201210
uom: Union[UCUMCode, URI] = Field(...)
202211

203212

204213
class CategoryRangeSchema(AnySimpleComponentSchema):
205214
type: Literal["CategoryRange"] = "CategoryRange"
206215
value: list[str] = Field(None)
207-
code_space: str = Field(None, serialization_alias='codeSpace')
216+
code_space: str = Field(None, alias='codeSpace')
217+
218+
219+
AnyComponent = Annotated[
220+
Union[
221+
DataRecordSchema, VectorSchema, DataArraySchema, MatrixSchema,
222+
DataChoiceSchema, GeometrySchema,
223+
BooleanSchema, CountSchema, QuantitySchema, TimeSchema,
224+
CategorySchema, TextSchema,
225+
CountRangeSchema, QuantityRangeSchema, TimeRangeSchema, CategoryRangeSchema,
226+
],
227+
Field(discriminator="type"),
228+
]

0 commit comments

Comments
 (0)