Skip to content

Commit d83abbd

Browse files
committed
Resolve serialization issues to/from JSON
1 parent 17e4029 commit d83abbd

File tree

3 files changed

+366
-57
lines changed

3 files changed

+366
-57
lines changed

src/mdio/core/v1/_serializer.py

Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
from mdio.schemas.v1.variable import Coordinate
2424
from mdio.schemas.v1.variable import Variable
2525
from mdio.schemas.v1.variable import VariableMetadata
26+
from mdio.schemas.chunk_grid import *
27+
from mdio.schemas.v1.stats import *
2628

2729
try:
2830
import zfpy as zfpy_base # Base library
@@ -46,13 +48,14 @@ def make_coordinate(
4648
metadata: list[AllUnits | UserAttributes] | None = None,
4749
) -> Coordinate:
4850
"""Create a Coordinate with the given name, dimensions, data_type, and metadata."""
49-
return Coordinate(
50-
name=name,
51-
long_name=long_name,
52-
dimensions=dimensions,
53-
data_type=data_type,
54-
metadata=metadata,
55-
)
51+
coordinate_dict = {
52+
"name": name,
53+
"longName": long_name,
54+
"dimensions": dimensions,
55+
"dataType": data_type,
56+
"metadata": metadata,
57+
}
58+
return Coordinate(**coordinate_dict)
5659

5760

5861
def make_variable( # noqa: PLR0913 PLR0912
@@ -81,52 +84,48 @@ def make_variable( # noqa: PLR0913 PLR0912
8184
Raises:
8285
TypeError: If the metadata type is not supported.
8386
"""
84-
# Convert metadata to VariableMetadata if needed
87+
88+
# TODO(BrianMichell) #0: I suspect that this is only partially correct...
89+
90+
def _to_serializable(val: Any) -> Any:
91+
return val.model_dump(mode="json", by_alias=True) if hasattr(val, "model_dump") else val
92+
8593
var_metadata = None
8694
if metadata:
8795
if isinstance(metadata, list):
88-
# Convert list of metadata to dict
8996
metadata_dict = {}
9097
for md in metadata:
9198
if isinstance(md, AllUnits):
92-
# For units_v1, if it's a single element, use it directly
93-
if isinstance(md.units_v1, list) and len(md.units_v1) == 1:
94-
metadata_dict["units_v1"] = md.units_v1[0]
95-
else:
96-
metadata_dict["units_v1"] = md.units_v1
99+
val = md.units_v1
100+
if isinstance(val, list) and len(val) == 1:
101+
val = val[0]
102+
metadata_dict["unitsV1"] = val
97103
elif isinstance(md, UserAttributes):
98-
# For attributes, if it's a single element, use it directly
99-
attrs = md.model_dump(by_alias=True)
100-
if isinstance(attrs, list) and len(attrs) == 1:
101-
metadata_dict["attributes"] = attrs[0]
102-
else:
103-
metadata_dict["attributes"] = attrs
104+
attrs = _to_serializable(md)
105+
metadata_dict["attributes"] = attrs[0] if isinstance(attrs, list) and len(attrs) == 1 else attrs
104106
var_metadata = VariableMetadata(**metadata_dict)
107+
105108
elif isinstance(metadata, dict):
106-
# Convert camelCase keys to snake_case for VariableMetadata
107109
converted_dict = {}
108110
for key, value in metadata.items():
109111
if key == "unitsV1":
110-
# For units_v1, if it's a single element array, use the element directly
111-
if isinstance(value, list) and len(value) == 1:
112-
converted_dict["units_v1"] = value[0]
113-
else:
114-
converted_dict["units_v1"] = value
112+
val = value[0] if isinstance(value, list) and len(value) == 1 else value
113+
converted_dict["unitsV1"] = _to_serializable(val)
115114
else:
116115
converted_dict[key] = value
117116
var_metadata = VariableMetadata(**converted_dict)
117+
118118
elif isinstance(metadata, VariableMetadata):
119119
var_metadata = metadata
120+
120121
else:
121-
msg = f"Unsupported metadata type: {type(metadata)}"
122-
raise TypeError(msg)
122+
raise TypeError(f"Unsupported metadata type: {type(metadata)}")
123123

124-
# Create the variable with all attributes explicitly set
125124
return Variable(
126125
name=name,
127-
long_name=long_name,
126+
longName=long_name,
128127
dimensions=dimensions,
129-
data_type=data_type,
128+
dataType=data_type,
130129
compressor=compressor,
131130
coordinates=coordinates,
132131
metadata=var_metadata,
@@ -140,12 +139,13 @@ def make_dataset_metadata(
140139
attributes: dict[str, Any] | None = None,
141140
) -> DatasetMetadata:
142141
"""Create a DatasetMetadata with name, api_version, created_on, and optional attributes."""
143-
return DatasetMetadata(
144-
name=name,
145-
api_version=api_version,
146-
created_on=created_on,
147-
attributes=attributes,
148-
)
142+
dataset_metadata_dict = {
143+
"name": name,
144+
"apiVersion": api_version,
145+
"createdOn": created_on,
146+
"attributes": attributes,
147+
}
148+
return DatasetMetadata(**dataset_metadata_dict)
149149

150150

151151
def make_dataset(

src/mdio/schemas/core.py

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,56 @@
88
from pydantic import BaseModel
99
from pydantic import ConfigDict
1010
from pydantic.alias_generators import to_camel
11+
from pydantic import Field
1112

1213

13-
def model_fields(model: type[BaseModel]) -> dict[str, tuple[Any, Any]]:
14-
"""Extract Pydantic BaseModel fields.
14+
# def model_fields(model: type[BaseModel]) -> dict[str, tuple[Any, Any]]:
15+
# """Extract Pydantic BaseModel fields.
1516

16-
Args:
17-
model: (Type) The model object for which the fields will be extracted.
17+
# Args:
18+
# model: (Type) The model object for which the fields will be extracted.
1819

19-
Returns:
20-
A dictionary containing the fields of the model along with
21-
their corresponding types and default values.
20+
# Returns:
21+
# A dictionary containing the fields of the model along with
22+
# their corresponding types and default values.
2223

23-
Example:
24-
>>> class MyModel(BaseModel):
25-
... name: str
26-
... age: int = 0
27-
...
28-
>>> model_fields(MyModel)
29-
{'name': (str, <default_value>), 'age': (int, 0)}
30-
"""
31-
annotations = get_type_hints(model)
24+
# Example:
25+
# >>> class MyModel(BaseModel):
26+
# ... name: str
27+
# ... age: int = 0
28+
# ...
29+
# >>> model_fields(MyModel)
30+
# {'name': (str, <default_value>), 'age': (int, 0)}
31+
# """
32+
# annotations = get_type_hints(model)
3233

33-
fields = {}
34-
for field_name, field in model.model_fields.items():
35-
fields[field_name] = (annotations[field_name], field)
34+
# fields = {}
35+
# for field_name, field in model.model_fields.items():
36+
# fields[field_name] = (annotations[field_name], field)
37+
38+
# return fields
3639

40+
# def model_fields(model: type[BaseModel]) -> dict[str, tuple[Any, Any]]:
41+
# """Return fields suitable for use in create_model with correct types and defaults."""
42+
# fields = {}
43+
# for field_name, field_info in model.model_fields.items():
44+
# annotated_type = field_info.annotation
45+
# default = field_info.default if field_info.default is not None else ...
46+
# fields[field_name] = (annotated_type, Field(default, description=field_info.description))
47+
# return fields
48+
49+
def model_fields(model: type[BaseModel]) -> dict[str, tuple[Any, Any]]:
50+
"""Safely extract fields for create_model, preserving optionality and default behavior."""
51+
fields = {}
52+
for field_name, field_info in model.model_fields.items():
53+
annotated_type = field_info.annotation
54+
if field_info.is_required():
55+
fields[field_name] = (annotated_type, ...)
56+
else:
57+
fields[field_name] = (
58+
annotated_type,
59+
Field(field_info.default, description=field_info.description),
60+
)
3761
return fields
3862

3963

@@ -46,4 +70,19 @@ class StrictModel(BaseModel):
4670
class CamelCaseStrictModel(StrictModel):
4771
"""A model with forbidden extras and camel case aliases."""
4872

49-
model_config = ConfigDict(alias_generator=to_camel)
73+
model_config = ConfigDict(
74+
extra="forbid",
75+
populate_by_name=False,
76+
alias_generator=to_camel,
77+
ser_json_by_alias=True,
78+
)
79+
80+
def model_dump_json(self, *args, **kwargs): # type: ignore[override]
81+
"""Dump JSON using camelCase aliases and excluding None values by default."""
82+
# Ensure camelCase aliases
83+
if "by_alias" not in kwargs:
84+
kwargs["by_alias"] = True
85+
# Exclude None fields to avoid nulls in output
86+
if "exclude_none" not in kwargs:
87+
kwargs["exclude_none"] = True
88+
return super().model_dump_json(*args, **kwargs)

0 commit comments

Comments
 (0)