Repository navigation
feat(python): add document loaders that reject a property set to null - #563
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Cyclic YAML aliases can crash the new loader with an unhandled recursion error.
2 open findings
What changed in this PR
Adds YAML/JSON document loaders that reject schema-invalid null properties while preserving lenient programmatic model construction.
Changes:
- Adds recursive null-property detection with
ai_contextexemptions. - Adds loader APIs and comprehensive serialization tests.
| File | Description |
|---|---|
python/src/ossie/models.py |
Implements YAML/JSON loaders and null detection. |
python/tests/test_serialization.py |
Tests loader behavior and schema-null handling. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
77e4d96 to
83f6145
Compare
83f6145 to
296a575
Compare
296a575 to
cd18c4a
Compare
cd18c4a to
25b30dd
Compare
25b30dd to
25687df
Compare
25687df to
2a4a792
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Accepted null-valued AI context extras are silently removed by default serialization.
5 open findings
Prevent exponential YAML alias expansion during JSON serialization Preserve None-valued ai_context extras during serialization · New Align null-path diagnostics with documented reporting behavior · New Reject non-JSON YAML types and non-string mapping keys Correct coverage explanation to use models with description fields · New
🧠 Review effort: Balanced
…ot express
No property in `ossie-schema.json` is nullable, but every optional field accepts
an explicit null, and `exclude_none=True` then drops the key: a null the schema
forbids becomes an omission it allows, so the mistake is never reported.
`{"is_time": null}` is rejected by the schema, accepted by the model, and
serialized as `{}`, which the schema accepts. A non-finite number launders the
same way, and `version` goes the other direction: the schema requires it, the
field carries a default, so a document without one gains one it never declared.
`model_validate` cannot be the place to fix any of it. Building a model in Python
routinely passes a value that may be None and means "omit it" -- `label=dim.label`
in the dbt converter, `description=data.get(...)` in Sigma -- and 143 call sites
across the converters do this, 119 at field level and 24 at document level, so
rejecting a null there would break callers rather than improve schema conformance.
The `version` default is wanted there for the same reason.
A document is different: it is the artefact the schema governs, and `to_ossie_yaml`
and `to_ossie_json` had no input counterpart. `from_ossie_yaml` and
`from_ossie_json` parse one and reject, naming the path to each:
- a property set to null;
- a non-finite number, since `yaml.safe_load` reads `.nan` and `.inf` and
`json.loads` accepts the non-standard `NaN`, `Infinity` and `-Infinity`, none of
which JSON expresses -- a NaN reaching `to_ossie_json` comes back out as `null`;
- a missing `version`;
- a cyclic alias, which `yaml.safe_load` resolves into a container holding itself.
The field types cover the rest of the schema. The free-form content of `ai_context`
is not audited: its object form sets `additionalProperties: true`, so a value
outside JSON's own data model -- a YAML set or date, a non-string mapping key --
is passed through and serialized as whatever it maps to, which is what
`model_validate` does today. A test pins that, so widening the contract is a
visible change rather than an accident.
Only the containers on the current branch count as a cycle, so an anchor reused on
a sibling branch still walks. A container reached again through another alias is
not walked or reported twice: an anchor is one property in the source, so one path
locates it, and a chain of anchors shaped `[*previous, *previous]` expands to 2^n
copies of the anchored subtree, so a single null inside it would otherwise yield
2^n paths. A long list of paths is truncated with a count.
Tested by setting every optional property in one document and nulling each of its
61 paths in turn, requiring every one to be rejected and comparing the reported
paths as exact entries, since `description` is also a substring of
`datasets[0].description`. The sample is kept complete by walking the models from
`OssieDocument` and checking each one's properties against the mapping at that
position, so adding a field fails here even where its name is already used on
another model. A null array item is rejected by the field type rather than by the
walk: unlike an optional property, it is not a value the model would accept and
then drop.
2a4a792 to
9dce930
Compare
jbonofre
left a comment
There was a problem hiding this comment.
LGTM. I checked the cycle detection, alias reuse, and null handling in ai_context, and they look all correct.
One nit: in _from_document, the message is built as f"property {reason}: ...". For non-infinite numbers it reads "property not a number JSON can represent, so it cannot round-trip: path", which is a bit ungrammatical imho. If you reword it, the tests that match on "cannot round-trip" will need updating too.



Summary
Followup from #557 covering other fields as well
Three rules in
ossie-schema.jsoncannot be expressed by the model fields, so the SDKaccepts documents the schema rejects and quietly rewrites them:
{"is_time": null}{}{"score": NaN}/.nannullversionrequired: [version, name, datasets]0.2.0.dev0Each is laundered rather than reported: the invalid input becomes valid output, so the
mistake disappears.
Why not in
model_validateBuilding a model in Python routinely passes a value that may be
Noneand means "omitit" —
label=dim.labelin dbt,description=data.get(...)in Sigma. An AST scan found143 such call sites, 119 at field level and 24 at document level, including
converter source:
sigma_to_ossie.py(19),wisdom_to_ossie.py(16),msi_to_ossie.py(9). Rejecting a null there breaks callers rather than improvingconformance, and the
versiondefault is wanted there for the same reason.What the loaders do
A document is the artefact the schema governs, and
to_ossie_yaml/to_ossie_jsonhad no input counterpart:
They reject, naming the path to each: a property set to null; a non-finite number
(yaml.safe_load reads .nan and .inf, json.loads accepts the non-standard NaN,
Infinity, -Infinity); a missing version; and a cyclic alias, which
yaml.safe_load resolves into a container holding itself.
property set to null, which the schema does not allow; omit it instead:
datasets[0].fields[0].dimension.is_time, metrics[0].datatype
A diagnostic lists at most 20 paths and counts the rest, so fixing the named ones can
reveal more on the next run. A container reached again through an alias is reported
once, at the path where the anchor is defined: an anchor is one property in the source,
and a chain shaped [*previous, *previous] expands to 2^n copies, so expanding every
occurrence is unbounded.
model_validate stays lenient, pinned by a test, so the converters keep working.
What they deliberately do not do
The free-form content of ai_context is not audited — its object form sets
additionalProperties: true. A value there outside JSON's data model (a YAML set or
date, a non-string mapping key) is passed through and serialized as whatever it maps
to, and a shared alias graph still expands on the way out. This is unchanged from
main, where model_validate accepts all of it:
!!set {a: null} -> ["a"] 2026-01-01 -> "2026-01-01" {1: one} -> {"1": "one"}
18 anchors in an ai_context extra: to_ossie_json takes 586 ms, emits 4,194,456 bytes
a null extra is dropped by exclude_none=True
A test pins that passthrough with the exact serialized forms, so widening the contract
later is a visible change. Auditing free-form value types, bounding output expansion
and preserving a null extra are properties of the models and the serializer rather than
of parsing; they affect main today and are tracked separately.
Related Issues
Checklist
Specification
core-spec/and follow the existing structureOntology
ontology/are consistent with spec changesConverters
converters/is updated to reflect spec or ontology changes.github/labeler.ymlis updated with the new pathValidation
validation/are updated if the spec changedDocumentation
docs/is updated to reflect any user-facing changesCONTRIBUTING.mdis updated if the contribution process changedExamples
examples/are added or updated for any new spec constructs or converter supportTests
pytest/ CI green)Compliance
NOTICEand/orLICENSEhave been updated per ASF policy