Skip to content

feat: preserve flexible JSON with schema-directed storage - #1062

Merged
nabinchha merged 16 commits into
mainfrom
codex/schema-json-storage
Oct 24, 2026
Merged

nabinchha merged 16 commits into
mainfrom
codex/schema-json-storage

Conversation

@nabinchha

@nabinchha nabinchha commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

📋 Summary

Structured JSON can pass generation validation but lose dictionary-key presence or fail when Arrow infers its Parquet schema. Add explicit JSON storage for selected schema nodes or columns, preserving logical Python values through generation and DD reads. Report proven storage failures and data-dependent risks during config validation, before model calls.

🔗 Related Issue

Fixes #1045. Builds on #1046, which is already merged; this PR targets main and contains the storage changes.

🔄 Changes

  • Support x-data-designer-storage: json on structured schema nodes and storage_encoding="json" for whole columns. Strip storage annotations from model-facing schemas while retaining them in saved configs and fingerprints.
  • Compile finite property/array/local-reference paths into a versioned contract stored in Parquet and run metadata. Write explicit UTF-8 leaves, including empty/null-only batches, and validate physical metadata before decoding.
  • Preserve contracts through results, previews, processors, DataFrame/local-file seeds, combined main/dropped reads, workflows, resume, and exports. Keep protected scalars out of pandas inference and preserve nested values across chunked DuckDB seed reads.
  • Share logical DataFrame construction between generation buffers and schema-transform outputs. Processor artifacts declare their own json_storage_columns; projected or dropped columns cannot leak their policy into reused names.
  • Check native-storage schemas during config validation: errors for proven failures, warnings for uncertain cases, and actionable column/path guidance. Respect encoded subtrees, processor/skip/drop context, schema dialect limits, and actual writer behavior.
  • Preserve exact JSON numbers and nulls through JSONL/CSV exports; normalize native nonfinite values inside structured CSV cells. Write artifacts through temporary files and atomic replacement so failed serialization preserves existing files.
  • Reject storage annotations beneath positional array items, including prefixes split across local references; preserve Unicode separators inside JSONL strings; skip empty schema-transform batches before checking output storage names.
  • Add schema, codec, preflight, lifecycle, resume, processor, seed, preview, and export regressions; document the options, storage boundaries, and implementation limits.

🔍 Attention Areas

  • This PR introduces a public schema annotation and a versioned persistent storage contract. Please review the names and compatibility promise before merge.
  • External Parquet readers see JSON text; DD readers decode it using file metadata. Successful unannotated writes retain existing struct normalization, while proven unsupported schemas now fail earlier.
  • Supported traversal is finite properties, homogeneous array items, and local references. Encode an enclosing node for branch-dependent, dynamic, or recursive shapes. Main-dataset processors must preserve declared paths; automatic remapping and native Arrow map compilation are outside this change.
  • User-owned DataFrames or plugin transformations that already coerced values cannot recover their original types from the storage flag.
  • Design and implementation status: plan.

🧪 Testing

  • Full engine suite: 2,755 passed.
  • Full config/interface suites: 2,038 passed, 3 skipped. Both full runs used this worktree at 39fe3d5e via pytest after addressing Andre's three inline comments.
  • 18 new regression cases cover positional array validation (direct and referenced schemas), enclosing-array/inactive-keyword compatibility, all three Unicode separators in root/nested JSONL strings, and empty-to-healthy processor batches in preview/batch mode. The 16 failure cases reproduced before the fixes and now pass.
  • Three independent sub-agents completed seven cycles of ten substantive passes each (210 passes), with fixes and regression tests between candidates. The campaign concluded with all three reviewers giving Ship it on ebbbf867, before subsequent human-review fixes; that verdict is historical and is not a fresh review of 39fe3d5e.
  • Real public create/preview pipelines and independent reproductions cover nullable scalars, precise/huge integers, mixed JSON, batch dependencies, processor artifacts, combined datasets, file/DataFrame seed handoffs, and all export formats. External model responses are stubbed where needed.
  • Earlier feature revision, minimum-version config run: 807 passed with Pydantic 2.9.2 and jsonschema 4.0.0. Storage/public JSON lifecycle run: 275 passed with current Pydantic and jsonschema 4.0.0.
  • All 31 changed Python files pass Ruff lint and formatting; commit hooks and git diff --check pass. Import checks keep pandas, pyarrow, duckdb, and jsonschema unloaded until needed.
  • Pinned [email protected] check reports no errors; internal links pass across 90 pages. Fern's remote redirects check was skipped because authentication was unavailable.
  • Full minimum-version engine matrix: blocked by an existing, unchanged telemetry class-variable incompatibility under Pydantic 2.9.2. No full engine lower-bound compatibility claim is made.
  • Live-provider/Hub E2E, remaining dependency/engine matrix combinations, and performance benchmarks have not been run.

Current environment: Python 3.13.3, Pydantic 2.12.5, jsonschema 4.26.0, pandas 3.0.5, pyarrow 24.0.0, DuckDB 1.5.0.

✅ Checklist

  • Follows commit message conventions.
  • Commits are signed off (DCO).
  • Contributor is a repository collaborator; external issue-triage requirement is not applicable.
  • Architecture, user documentation, and implementation plan updated.

Description updated with AI

Preserve opted-in JSON values across Parquet artifacts, resume, seed handoffs, processors, and exports. Add preflight storage diagnostics before model calls and lifecycle regression coverage.

Refs #1045

Signed-off-by: Nabin Mulepati <[email protected]>
@github-actions

Copy link
Copy Markdown
Contributor

Fern preview: https://nvidia-preview-pr-1062.docs.buildwithfern.com/nemo/datadesigner

Fern previews include the docs-website version archive with PR changes synced into latest. Notebook tutorials are rendered without execution outputs in previews.

Address independent schema, storage, and lifecycle reviews with regressions for reference scopes, container alternatives, null and projected seed handoffs, partitioned datasets, malformed contracts, and effective storage policy.

Signed-off-by: Nabin Mulepati <[email protected]>
Keep annotated DataFrame seed tables typed and contiguous through DuckDB. Match native pandas inference in preflight, normalize native nulls and strict CSV cells, and validate literal processor storage names before generation.

Signed-off-by: Nabin Mulepati <[email protected]>
@nabinchha
nabinchha marked this pull request as ready for review October 21, 2026 23:37
@nabinchha
nabinchha requested a review from a team as a code owner October 21, 2026 23:37
@greptile-apps

greptile-apps Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge, with all previous findings addressed and no new actionable issues found.

Summary

This PR adds opt-in JSON storage for schema nodes and whole columns while keeping ordinary Python values inside DD.

  • Structured schemas can mark JSON fields that Data Designer stores as JSON text.
  • Saved artifacts keep selected JSON values in explicit text leaves.
  • Data Designer readers and exports restore values using each artifact's storage plan.
  • Schema-transform processors can store chosen output columns as JSON.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[Column config and schema] --> B[Storage plan]
    A --> C[Model-facing schema without storage annotations]
    C --> D[Logical Python values]
    D --> E[Encode selected values]
    B --> E
    E --> F[Parquet with versioned metadata]
    F --> G[DD reader decodes values]
    G --> H[Results, processors, and seeds]
Loading

Reviews (8) · Last reviewed commit: "refactor: use verb-led JSON storage help..." · Reviewed by Greptile

Comment on lines +122 to +126
items_required = (
certain and nonnull_types == {"array"} and node.get("minItems", 0) > len(node.get("prefixItems", []))
)
if "items" in node:
visit(node["items"], (*path, None), items_required, refs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Writable arrays cannot start

items_required can turn a writable array schema into a startup error. An array with prefixItems requiring an object with integer field x, trailing items: {"const": {}}, and minItems: 2 accepts [{"x": 1}, {}]. Arrow gives both elements the same struct type, so the first element supplies x and the array can be written.

The check tests the trailing {} alone and marks it as a definite failure, blocking generation. Keep this warning-only unless the check accounts for fields supplied by other array elements.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/data-designer-engine/src/data_designer/engine/storage/schema_preflight.py
Line: 122-126

Comment:
**Writable arrays cannot start**

`items_required` can turn a writable array schema into a startup error. An array with `prefixItems` requiring an object with integer field `x`, trailing `items: {"const": {}}`, and `minItems: 2` accepts `[{"x": 1}, {}]`. Arrow gives both elements the same struct type, so the first element supplies `x` and the array can be written.

The check tests the trailing `{}` alone and marks it as a definite failure, blocking generation. Keep this warning-only unless the check accounts for fields supplied by other array elements.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 82b17cda. Trailing-item findings are now warning-only when a nonempty prefixItems is present: prefix elements can supply struct fields that make otherwise unwritable trailing values serializable. Added regressions for the writable [{"x": 1}, {}] example and a longer array; both validate against the schema and write successfully through the actual pandas/Parquet writer. Both cases were incorrectly rejected before the fix. Updated the existing positional-array test to preserve this conservative behavior.

Validation: the two targeted preflight/context suites pass all 122 tests. The full config, engine, and interface suites pass 4,875 tests with 3 skipped. Ruff lint/format, diff checks, and commit hooks pass.

Comment on lines +109 to +112
uncertain_output = any(
processor.processor_type not in (ProcessorType.DROP_COLUMNS, ProcessorType.SCHEMA_TRANSFORM)
for processor in processors
) or any(column.skip is not None for column in columns)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Unrelated skips hide storage errors

A skip on any column downgrades storage errors for every structured column. With an independent structured column using output_format={"const": {}}, adding a skip to an unrelated column changes its proven storage failure into a warning.

Skipping the unrelated column cannot change the empty object, so generation proceeds and fails when saving the batch. Apply the skip-based downgrade only to columns whose values can actually be skipped.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/data-designer-engine/src/data_designer/engine/validation.py
Line: 109-112

Comment:
**Unrelated skips hide storage errors**

A `skip` on any column downgrades storage errors for every structured column. With an independent structured column using `output_format={"const": {}}`, adding a `skip` to an unrelated column changes its proven storage failure into a warning.

Skipping the unrelated column cannot change the empty object, so generation proceeds and fails when saving the batch. Apply the skip-based downgrade only to columns whose values can actually be skipped.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 82b17cda. Skip uncertainty is now limited to columns that can actually skip, either through their own gate or propagated data dependencies. The analysis follows transitive dependencies and producer side effects, respects propagate_skip=False, and leaves independent structured-column failures as errors. Added regressions for unrelated skips, direct/transitive propagation, propagation barriers, and side-effect dependencies. Unknown plugin processors retain the existing conservative downgrade.

Validation: the two targeted preflight/context suites pass all 122 tests. The full config, engine, and interface suites pass 4,875 tests with 3 skipped. Ruff lint/format, diff checks, and commit hooks pass.

@github-actions

Copy link
Copy Markdown
Contributor

Thanks for this, @nabinchha. The PR is large and careful, and the attention-areas section made it much easier to review.

Summary

The PR adds opt-in JSON storage for structured values. It works through three things: an x-data-designer-storage: json schema annotation, a whole-column storage_encoding="json" field, and processor-level json_storage_columns. These compile into a versioned contract (data_designer.json_storage, v1) that is embedded in Parquet metadata and run metadata, and that contract is carried through generation, seeds, previews, resume, workflows and exports. It also adds config-time "preflight" checks that report Parquet storage failures before any model spend. The implementation matches the stated intent. I ran the PR's own storage, schema, fingerprint, row-buffer and interface tests locally (478 passed), and Ruff lint and format are clean on all 31 changed Python files.

Findings

Warnings — Worth addressing

packages/data-designer-config/src/data_designer/config/utils/io_helpers.py:126 — read_parquet_dataset now reads directories differently for every caller

  • What: The directory branch moved from a non-recursive sorted(path.glob("*.parquet")) to pq.ParquetDataset(path). I confirmed two behavior changes locally:
    • A Parquet file in a subdirectory is now included (3 rows read instead of 2).
    • A stray non-Parquet file in the directory, such as notes.txt, now raises ArrowInvalid. Before, it was ignored.
  • Why: This function sits behind ArtifactStorage.load_dataset, load_processor_dataset, combined main/dropped reads, CSV export, and now composite_workflow._load_parquet_dataset. The workflow helper becomes internally inconsistent: _parquet_files and _count_parquet_records still use top-level *.parquet only, while the load path now recurses and also picks up Hive partition columns. The new read_encoded_parquet path has the same recursion. Users who point a workflow stage or seed at a directory with extra files would see a silent change. That isn't mentioned in the PR description.
  • Suggestion: Keep the old file selection and pass the explicit list, e.g. lazy.pq.ParquetDataset(sorted(path.glob("*.parquet"))). That keeps fragment metadata access and drops the recursion and partitioning side effects. A regression test with a sibling .txt file and a nested subdirectory would lock this in.

packages/data-designer-engine/src/data_designer/engine/storage/schema_preflight.py:93 — New warnings on common Pydantic shapes for every existing structured config

  • What: The new warning-level check fires for a plain Pydantic model with addr: Optional[Addr] = None and tags: dict[str, str] = {}. It produces two PARQUET_STORAGE warnings: "combines schema branches…" and "allows dynamic object keys…". validate_data_designer_config runs these on every compile, and rich_print_violations prints them.
  • Why: Optional nested models and string dictionaries are very common in output_format models. Every existing user with those shapes now gets warnings about a feature they haven't opted into, even when their data writes fine. That noise teaches people to ignore real warnings.
  • Suggestion: Could we exempt the common nullable-object union? anyOf: [{"$ref": ...}, {"type": "null"}] is the same shape _is_nullable_scalar_union already exempts for scalars, extended to object refs. We could also drop the dynamic-keys case to an info/debug log unless it is proven to fail. Alternatively, collapse all preflight warnings for a column into one message. A test asserting that a typical Optional[Model] + dict[str, str] model produces no warnings would help.

packages/data-designer-engine/src/data_designer/engine/resources/seed_reader.py:276 — get_storage_plan adds eager remote I/O and leaks raw exceptions

  • What: DatasetBuilder.__init__ calls seed_reader.get_storage_plan() unconditionally. For Hugging Face .parquet sources, that globs the repo and opens every matching file over the network to read its schema, just to look for DD metadata. For local sources, pq.read_schema errors such as a corrupt file or a missing glob match surface as raw Arrow/OS/HF exceptions. They aren't wrapped in SeedReaderError.
  • Why: This adds a per-file round trip for large sharded HF seeds, even though almost none of them will carry DD metadata. It also breaks the "errors normalize at boundaries" principle. Builder construction can now fail with a third-party exception type.
  • Suggestion: Wrap the discovery in try/except and re-raise as SeedReaderError. For HF, consider reading only the first file's footer and validating the rest lazily on read, or making discovery opt-in. Separately, isinstance(seed_plan, JsonStoragePlan) in dataset_builder.py:209 reads as a defensive guard for mocks. Since get_storage_plan is typed and always returns a plan, it can go.

Suggestions — Take it or leave it

Public contract naming — one concept, three spellings

  • What: The same idea appears as storage_encoding: Literal["json"] on columns, json_storage_columns: list[str] on SchemaTransformProcessorConfig, and x-data-designer-storage: "json" in schemas.
  • Why: You asked for review of the names before merge. All three, plus the v1 metadata key, become compatibility commitments once they ship.
  • Suggestion: One shared vocabulary would be easier to discover. For example, the processor could take storage_encoding: dict[str, Literal["json"]], or columns could expose json_storage: bool. Since SingleColumnConfig.storage_encoding applies to every column type, it may also be worth saying in the field description which column types it is meant for.

packages/data-designer-engine/src/data_designer/engine/processing/gsonschema/schema_transformers.py:28 — Silent relaxation of _is_bare_dictionary_schema

  • What: Before, only {type} or {type, title} counted as a bare dictionary. Now description, examples and default qualify too. As a result, {"type": "object", "description": "..."} changes from implied additionalProperties: false to true.
  • Why: This is arguably a bug fix: before, such a schema accepted only {}. But it changes validation for existing users, it isn't related to storage apart from the new key, and it isn't called out in the PR description.
  • Suggestion: Mention it in the PR description, or add a focused test with a short comment explaining why these keys are safe.

packages/data-designer-config/src/data_designer/config/utils/storage_encoding.py:198 — deepcopy per path per row

  • What: _transform_value deep-copies the whole object at every traversal step. A row is then re-walked and re-copied once for each declared path.
  • Why: For wide schemas with many annotated leaves and large batches, copying grows roughly as paths × object size per row, on the hot write and read path.
  • Suggestion: Do a single deepcopy per cell at the top of the encode/decode loop, then transform in place with shallow dict(value) copies. Or group paths by their first segment so each subtree is traversed once.

Layering note — Parquet codec lives in data_designer.config.utils

  • What: storage_encoding.py does Arrow table building, Parquet I/O and the encode/decode lifecycle inside the config package.
  • Why: There is precedent (io_helpers.read_parquet_dataset), and it's needed so LocalFileSeedSource.from_dataframe and PreviewResults can keep the contract. Still, it moves more runtime behavior into the config package's "configs are data" layer.
  • Suggestion: No change needed for this PR. Worth a sentence in architecture/dataset-builders.md explaining why the codec sits in config, so later changes don't keep growing it there.

What Looks Good

  • Atomic artifact writes: write_parquet_file writes to a temporary file and then calls os.replace, with ArtifactStorageError messages that name the failing column and point to the fix. That's a real robustness gain, independent of the JSON feature.
  • Strict contract validation: Contracts are checked against the physical schema on read (read_storage_plan / _validate_storage_path), and runs refuse to resume across a contract mismatch. Fingerprint normalization (dropping storage_encoding=None and empty json_storage_columns) keeps existing config hashes stable.
  • Test coverage: The tests cover the full lifecycle: codec, preflight, processor, seed, preview, resume and export. Empty and null-only batches, exact integers and chunk boundaries are all exercised explicitly.

Residual Risk

  • I did not run the full engine/interface suites or a live end-to-end run; I relied on the PR's reported results for those. Performance of the encode/decode path on large batches has not been benchmarked, as the PR notes.

Structural Impact

Note: the four reported "import direction violations" are indirect_call edges inferred from method-name collisions (e.g. .tail(), .value(), .model_alias()). I found no real reverse imports in the diff: config only imports config, and engine imports config.

Risk: HIGH (4 core abstraction(s) modified; 4 import direction violation(s))

  • 31 Python files, 802 AST entities, 46/196 clusters

Import Direction Violations (4)

Legal direction: interface -> engine -> config

  • .get_model_aliases() (config) --indirect_call--> .model_alias() (engine)
  • _transform_value() (config) --indirect_call--> .tail() (engine)
  • _validate_storage_path() (config) --indirect_call--> .tail() (engine)
  • _request_resource_label() (engine) --indirect_call--> .value() (interface)

Core Abstractions Modified

High-Connectivity Changes

  • AsyncTaskScheduler (155 deps) in packages/data-designer-engine/src/data_designer/engine/dataset_builders/async_scheduler.py
  • async_scheduler.py (105 deps) in packages/data-designer-engine/src/data_designer/engine/dataset_builders/async_scheduler.py
  • dataset_builder.py (85 deps) in packages/data-designer-engine/src/data_designer/engine/dataset_builders/dataset_builder.py
  • composite_workflow.py (79 deps) in packages/data-designer/src/data_designer/interface/composite_workflow.py
  • ConfigBase (73 deps) in packages/data-designer-config/src/data_designer/config/base.py
  • DatasetBuilder (65 deps) in packages/data-designer-engine/src/data_designer/engine/dataset_builders/dataset_builder.py
  • ArtifactStorage (63 deps) in packages/data-designer-engine/src/data_designer/engine/storage/artifact_storage.py
  • seed_reader.py (61 deps) in packages/data-designer-engine/src/data_designer/engine/resources/seed_reader.py
  • +318 more

Cross-Package Dependencies

  • GenerationController (interface) --uses--> PreviewResults (config)
  • ._save_preview_results() (interface) --references--> PreviewResults (config)
  • ._display_record_with_header() (interface) --references--> PreviewResults (config)
  • ._browse_records_interactively() (interface) --references--> PreviewResults (config)
  • ._display_all_records() (interface) --references--> PreviewResults (config)
  • ._validate_endpoint() (interface) --calls--> is_http_url() (config)
  • +271 more

Verdict

Needs changes. Please address these before merge:

  • Restore the non-recursive, Parquet-only file selection in read_parquet_dataset.
  • Reduce the preflight warning noise for common Optional[Model] and dict[str, str] shapes.
  • Normalize errors from seed storage-plan discovery and bound its remote I/O.

The naming question in Suggestions is worth settling before the v1 contract ships.


This review was generated by an AI assistant.

unsupported or types is None or types - {"null"} != {"object"},
paths,
)
elif key == "items" and isinstance(value, dict):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

items excludes the entries covered by prefixItems, but the compiled wildcard visits every element. An integer prefix followed by an object with JSON-encoded args accepts [1, {"args": {}}], then fails during checkpoint writing when traversal reaches the integer. Rejecting this placement during config validation and asking for encoding on the enclosing array would catch it before generation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 39fe3d5e. The compiler now rejects storage annotations beneath positional items during config validation, including when prefixItems and items are split across local references. Added regressions for nested-field and whole-item annotations, plus checks that encoding the enclosing array remains supported.

# Keep encoded leaves as text through pandas' JSON conversion,
# which rounds floats. Decode them only after native fields have
# received the usual date/null normalization.
records = [json.loads(line) for line in content.splitlines()]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A string containing U+0085, U+2028, or U+2029 makes JSONL export fail here. Pandas leaves those characters unescaped, so splitlines() breaks a valid JSON record inside the string. Splitting only on "\n" and ignoring the trailing empty entry would preserve these values.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 39fe3d5e. JSONL decoding now splits only on \n and ignores the trailing empty entry. Added round-trip tests for U+0085, U+2028, and U+2029 in protected root/nested JSON and native string fields across multiple batches.


def process_after_batch(self, data: pd.DataFrame, *, current_batch_number: int | None) -> pd.DataFrame:
formatted_data = self._transform(data)
missing = set(self.config.json_storage_columns) - set(formatted_data.columns)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When every row in a batch is dropped, _transform() returns an empty frame without columns. Even a constant template like {"payload": {"k": "v"}} then raises this error with json_storage_columns=["payload"], aborting the run before later healthy batches complete. Skip empty inputs before this check, or retain the declared output columns when constructing an empty result.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 39fe3d5e. Schema transforms now return immediately for inputs with zero rows, before transformation and output-column checks. Added preview/batch regressions with literal and dynamic output names that process an empty input followed by a healthy batch and verify the healthy artifact is written.

Reject positional item annotations before generation, preserve Unicode
separators in JSONL records, and skip empty schema-transform batches.

Add regressions for local references, nested JSON, and processor outputs.

Signed-off-by: Nabin Mulepati <[email protected]>
reference = node.get("$ref")
if reference is None or reference in references:
return False
target = resolve_local_json_pointer(root, reference)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One non-blocking edge case: this helper follows references inside nodes already marked for JSON storage. A referenced, annotated definition containing a named-anchor reference like #payload now fails config validation, although its contents should be opaque to the storage compiler. Returning False at annotated nodes would match _get_possible_types(). This requires an unusual custom schema, so a follow-up is fine.

andreatnvidia
andreatnvidia previously approved these changes Oct 22, 2026

@andreatnvidia andreatnvidia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for fixing these, @nabinchha. The three issues I raised are addressed. The remaining custom-schema edge case is non-blocking, so I’m happy to approve.

Comment on lines +110 to +111
if node.get("maxProperties") == 0 or closed_empty:
report(path, "requires an empty object, which native Parquet cannot store as a struct", object_certain)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Writable empty objects are rejected

find_storage_schema_risks rejects empty objects even when an annotated child makes them writable. For example, {"type":"object","maxProperties":0,"properties":{"args":{"x-data-designer-storage":"json"}}} accepts {}. The encoder adds an explicit string field for args, so Parquet can store that value. This check still reports a definite failure and stops generation. Account for fields added by the storage plan before declaring an empty object unwritable.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/data-designer-engine/src/data_designer/engine/storage/schema_preflight.py
Line: 110-111

Comment:
**Writable empty objects are rejected**

`find_storage_schema_risks` rejects empty objects even when an annotated child makes them writable. For example, `{"type":"object","maxProperties":0,"properties":{"args":{"x-data-designer-storage":"json"}}}` accepts `{}`. The encoder adds an explicit string field for `args`, so Parquet can store that value. This check still reports a definite failure and stops generation. Account for fields added by the storage plan before declaring an empty object unwritable.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 82b17cda. Preflight now accounts for explicit physical fields supplied by JSON-encoded descendants instead of rejecting their empty logical ancestors. Added root, nested, and local-reference regressions that validate the empty sample against its schema, write it through the actual storage encoder to Parquet, and confirm validation accepts the schema. All three failed before the fix and now pass.

Validation: the two targeted preflight/context suites pass all 122 tests. The full config, engine, and interface suites pass 4,875 tests with 3 skipped. Ruff lint/format, diff checks, and commit hooks pass.

Account for physical fields supplied by encoded descendants and
positional array prefixes before declaring storage failures.
Limit skip uncertainty to affected columns and their dependents.

Add regression coverage and reconcile the implementation plan with
the delivered contract and recorded validation results.

Signed-off-by: Nabin Mulepati <[email protected]>
andreatnvidia
andreatnvidia previously approved these changes Oct 23, 2026
self.columns[name] = sorted(paths, key=_storage_path_sort_key)
return self

def for_columns(self, names: list[str]) -> JsonStoragePlan:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Helper names break convention

JsonStoragePlan.for_columns does not start with an action verb, as the repository guide requires. Rename it to restrict_to_columns or another verb-led name and update its callers. The same issue appears in _storage_type and _literal_kind. These names must meet the repository requirement before merging.

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/data-designer-config/src/data_designer/config/utils/storage_schema.py
Line: 70

Comment:
**Helper names break convention**

`JsonStoragePlan.for_columns` does not start with an action verb, as the repository guide requires. Rename it to `restrict_to_columns` or another verb-led name and update its callers. The same issue appears in `_storage_type` and `_literal_kind`. These names must meet the repository requirement before merging.

**Context Used:** AGENTS.md ([source](https://github.com/nvidia-nemo/datadesigner/blob/main/AGENTS.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 557cc5b8, following the verb-led naming convention in STYLEGUIDE.md.

  • JsonStoragePlan.for_columns → restrict_to_columns
  • _storage_type → _build_storage_type
  • _literal_kind → _get_literal_kind

Updated all callers, including recursive calls and seed-contract tests. This is a naming-only change; storage behavior and persisted metadata are unchanged.

Validation: 467 existing tests passed across the schema compiler, codec, engine storage/preflight/seed suites, row-group buffering, and public interface JSON-storage lifecycle tests. Ruff lint/format, git diff --check, and commit hooks also pass.

@nabinchha
nabinchha merged commit 207eda6 into main Oct 24, 2026
69 checks passed
@nabinchha
nabinchha deleted the codex/schema-json-storage branch October 24, 2026 02:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parquet checkpointing adds null dictionary entries and rejects mixed JSON arrays

2 participants