Skip to content

Complete JSON Schema reconstruction and current parity - #1

Closed
binaryfire wants to merge 11 commits into
0.4from
audit/json-schema-correctness-parity
Closed

Complete JSON Schema reconstruction and current parity#1
binaryfire wants to merge 11 commits into
0.4from
audit/json-schema-correctness-parity

Conversation

@binaryfire

@binaryfire binaryfire commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

This completes the JSON Schema package's current builder surface and makes schema reconstruction explicit, bounded, and reversible.

The package could already build and serialize a useful subset of JSON Schema, but it could not safely reconstruct that subset from arrays. The previous reference path recursively copied schemas, silently weakened several unsupported forms, and could consume enough memory to terminate a long-lived worker. Serialization also lost some valid falsey values and object-shaped boundaries.

What changed

  • Add JsonSchema::fromArray() and a dedicated deserializer for the subset represented by the package.
  • Add union and anyOf builders, including closure-based factory forms and explicit defaults.
  • Restore current builder behavior for unique arrays and reversible nullable, required, and additional-properties flags.
  • Resolve local references iteratively with per-operation caches, a bounded active reference path, and an aggregate expansion limit.
  • Reject malformed or unsupported assertions when reconstruction would otherwise produce a weaker schema.
  • Preserve valid permissive forms, nullable schemas, empty enums, falsey defaults, numeric property names, and JSON object boundaries.
  • Preserve JSON encoding failures instead of publishing an empty schema.
  • Publish a complete package guide covering construction, serialization, reconstruction, references, supported keywords, and extension boundaries.

Design

Reconstruction is operation-local. It does not add container state, coroutine state, locks, I/O, or worker-lived caches. Local $ref targets are cached only for one fromArray() call, active reference paths are limited to 256 distinct targets, and total schema expansion is capped at 20,000 fragments.

The deserializer is intentionally not a general JSON Schema validator. It accepts annotations and extensions that do not change validation, reconstructs the forms the builders can represent faithfully, and fails when a recognized assertion would be dropped or coerced. Applications can continue using a validator for arbitrary third-party schemas.

The existing Laravel-facing builder APIs and named arguments remain intact. The new APIs are additive; newly rejected inputs were previously malformed, unsupported, or reconstructed inaccurately.

Validation

  • Ran the complete JSON Schema test suite, including counterfactual reconstruction, reference-bound, composition, default, and serialization coverage.
  • Ran formatting and both PHPStan configurations.
  • Ran the repository's complete parallel test gate, Testbench package mode, and dogfood checks.
  • Reviewed the public contract, package metadata, documentation, callers, failure paths, lifecycle ownership, and hot-path behavior.

For the detailed implementation design and supported/rejected boundaries, see docs/plans/2026-08-07-2015-json-schema-correctness-current-parity-and-bounded-reconstruction.md.

Summary by CodeRabbit

  • New Features
    • Added support for JSON Schema unions, anyOf compositions, and reconstruction from schema arrays.
    • Added bounded local reference resolution with validation for unsupported or malformed schemas.
    • Added array uniqueness controls and improved object, nullable, required-property, and default-value handling.
  • Bug Fixes
    • Corrected schema normalization and preserved explicit null defaults.
    • Preserved JSON encoding errors instead of silently masking failures.
  • Documentation
    • Added comprehensive JSON Schema guidance, supported-feature details, limitations, and navigation links.

Add the first user-facing guide for building, serializing, and reconstructing JSON schemas.

Cover primitive and structured schemas, metadata, required and nullable properties, unions, any-of schemas, local references, and the supported reconstruction subset.
Preserve explicit null defaults independently from unset defaults, make required, nullable, and unique flags reversible, and retain valid empty enum values.

Emit numeric property maps and list-shaped object defaults as JSON objects, validate final union and any-of output after nullability is applied, and preserve JSON encoding failures with JSON_THROW_ON_ERROR. Add focused regression coverage for every corrected type and wire shape.
Expose multi-type unions and constrained any-of alternatives through the JSON Schema factory and contract using concrete, Laravel-style return types.

Normalize null union members into nullability, reject unsupported or non-string members without coercion, preserve member order, and support shared metadata and explicit defaults. Cover direct construction, closures, nullability, failure paths, and round trips.
Add JsonSchema::fromArray() and rebuild the supported JSON Schema 2020-12 subset without silently weakening recognized validation rules or malformed input.

Resolve local references iteratively with per-operation caching, a bounded active path, and a bounded aggregate expansion count. Preserve representable null, composition, enum, default, object-map, and permissive-items forms while rejecting circular, remote, lossy, or structurally conflicting schemas.

Exercise every supported type, nested and referenced schemas, exact round trips, resource bounds, invalid keyword values, unsupported assertions, integer limits, and composition failure paths.
Add the JSON Schema guide to the framework documentation navigation and link the package README to the canonical user documentation.

Record the public differences from Laravel that developers must account for, including explicit null defaults, sum-type defaults, strict reconstruction failures, supported null and items forms, and bounded local reference expansion.
Mark the package complete in the framework audit checklist and route future readers to the dedicated JSON Schema plan and ledger entry.

Record the accepted findings, rejected speculative mechanisms, lifecycle and performance assessment, regression coverage, upstream handoff, validation results, and final no-debt disposition.
Capture the verified Laravel parity surface, serializer and reconstruction defects, approved API improvements, bounded-reference design, and explicit anti-overengineering constraints.

Describe the final implementation by owning boundary, include representative code, define the regression and full-gate validation plan, and preserve the completed audit decisions needed for future maintenance.
Rewrite the guide in the simple, direct prose used by first-party Laravel documentation while preserving the original baseline in commit history.

Clarify reversible constraints, supported union members, serialization failures, reference limits, nullable one-of reconstruction, permissive array forms, malformed inputs, unsupported keywords, and valid schema forms the fluent builder cannot preserve.
…ectness-parity

# Conflicts:
#	docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md
…ectness-parity

# Conflicts:
#	docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md
#	docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 83a85ed0-1f66-49b1-b16f-3c809a4042c9

📥 Commits

Reviewing files that changed from the base of the PR and between 62f4976 and bf78c85.

📒 Files selected for processing (6)
  • docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md
  • docs/plans/2026-08-07-2015-json-schema-correctness-current-parity-and-bounded-reconstruction.md
  • src/boost/docs/json-schema.md
  • src/json-schema/README.md
  • src/json-schema/src/Deserializer.php
  • tests/JsonSchema/DeserializerTest.php
🚧 Files skipped from review as they are similar to previous changes (4)
  • docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md
  • tests/JsonSchema/DeserializerTest.php
  • src/json-schema/src/Deserializer.php
  • src/boost/docs/json-schema.md

📝 Walkthrough

Walkthrough

The JSON Schema package adds union and any-of types, raw schema reconstruction, bounded local reference resolution, explicit default tracking, stricter serialization errors, documentation, audit records, and regression tests.

Changes

JSON Schema parity and reconstruction

Layer / File(s) Summary
Schema contracts and serialization
src/contracts/src/JsonSchema/JsonSchema.php, src/json-schema/src/JsonSchema.php, src/json-schema/src/JsonSchemaTypeFactory.php, src/json-schema/src/Types/*, src/json-schema/src/Serializer.php
Adds union(), anyOf(), fromArray(), UnionType, and AnyOfType. Tracks explicit defaults, supports array uniqueness, normalizes object output, and propagates JSON encoding errors.
Bounded schema deserialization
src/json-schema/src/Deserializer.php
Reconstructs supported schemas, handles compositions and nullability, resolves local references, validates constraints, and enforces depth and expansion limits.
Composition, reconstruction, and error coverage
tests/JsonSchema/*
Adds coverage for new types, round trips, defaults, object normalization, references, malformed schemas, unsupported assertions, encoding failures, and expansion boundaries.
Documentation and audit records
src/boost/docs/*, src/json-schema/README.md, docs/plans/*
Documents the API, supported reconstruction subset, errors, limits, Laravel differences, and completed audit status.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant JsonSchema
  participant Deserializer
  participant LocalReferenceResolver
  participant Type
  Caller->>JsonSchema: call fromArray(schema)
  JsonSchema->>Deserializer: deserialize(schema)
  Deserializer->>LocalReferenceResolver: resolve local $ref when present
  LocalReferenceResolver-->>Deserializer: return bounded schema fragment
  Deserializer->>Type: construct reconstructed type
  Type-->>Caller: serialize reconstructed schema
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: completing JSON Schema reconstruction and current Laravel parity.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch audit/json-schema-correctness-parity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 8, 2026

Copy link
Copy Markdown

Greptile Summary

The PR completes the JSON Schema builder and adds bounded, loss-aware reconstruction from arrays.

  • Adds union and anyOf builders with explicit default handling.
  • Adds strict deserialization with iterative local-reference resolution and expansion limits.
  • Preserves falsey defaults, object-shaped boundaries, nullability, and reversible builder flags.
  • Adds extensive reconstruction, composition, serialization, and reference-boundary tests and documentation.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/json-schema/src/Deserializer.php Adds bounded reconstruction and now correctly rejects nullable oneOf schemas containing duplicate resolved bare-null branches or another null-accepting branch.
tests/JsonSchema/DeserializerTest.php Covers duplicate inline and referenced null branches, null-overlapping surviving branches, composition ownership, malformed assertions, and reference limits.
src/json-schema/src/Serializer.php Preserves explicit null defaults and object-shaped boundaries while surfacing JSON encoding and empty-composition failures.
src/json-schema/src/Types/UnionType.php Adds strict union-member validation, null normalization, deduplication, and mixed defaults.
src/json-schema/src/Types/AnyOfType.php Adds the composition builder representation and explicit mixed defaults.

Reviews (2): Last reviewed commit: "fix(json-schema): preserve nullable comp..." | Re-trigger Greptile

Comment thread src/json-schema/src/Deserializer.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@docs/plans/2026-08-07-2015-json-schema-correctness-current-parity-and-bounded-reconstruction.md`:
- Around line 439-447: Reorder the workflow in the post-implementation checklist
so the explicit owner-approval gate in the final instruction occurs before
implementation, validation, review sign-off, and all ledger, routing, result,
defect-summary, and completion records. Alternatively, clearly label those
records as post-approval archival actions, while preserving the prohibition on
creating any source, test, documentation, ledger, or bookkeeping commit before
approval.

In `@src/json-schema/src/Types/AnyOfType.php`:
- Around line 14-17: Reject empty composition inputs in AnyOfType::__construct
by throwing InvalidArgumentException before or during normalization of $schemas,
and apply the same validation in UnionType::__construct for empty $types. Update
src/json-schema/src/Types/AnyOfType.php lines 14-17 and
src/json-schema/src/Types/UnionType.php lines 32-55; retain existing behavior
for non-empty inputs.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 93c89d49-b002-4abf-b3bd-41ff41327a67

📥 Commits

Reviewing files that changed from the base of the PR and between 65ce5f1 and 62f4976.

📒 Files selected for processing (31)
  • docs/plans/2026-07-12-0900-framework-coroutine-state-lifecycle-audit.md
  • docs/plans/2026-07-12-0915-framework-coroutine-state-lifecycle-audit-ledger.md
  • docs/plans/2026-08-07-2015-json-schema-correctness-current-parity-and-bounded-reconstruction.md
  • src/boost/docs/documentation.md
  • src/boost/docs/json-schema.md
  • src/contracts/src/JsonSchema/JsonSchema.php
  • src/json-schema/README.md
  • src/json-schema/src/Deserializer.php
  • src/json-schema/src/JsonSchema.php
  • src/json-schema/src/JsonSchemaTypeFactory.php
  • src/json-schema/src/Serializer.php
  • src/json-schema/src/Types/AnyOfType.php
  • src/json-schema/src/Types/ArrayType.php
  • src/json-schema/src/Types/BooleanType.php
  • src/json-schema/src/Types/IntegerType.php
  • src/json-schema/src/Types/NumberType.php
  • src/json-schema/src/Types/ObjectType.php
  • src/json-schema/src/Types/StringType.php
  • src/json-schema/src/Types/Type.php
  • src/json-schema/src/Types/UnionType.php
  • tests/JsonSchema/AnyOfTypeTest.php
  • tests/JsonSchema/ArrayTypeTest.php
  • tests/JsonSchema/BooleanTypeTest.php
  • tests/JsonSchema/DeserializerTest.php
  • tests/JsonSchema/IntegerTypeTest.php
  • tests/JsonSchema/NumberTypeTest.php
  • tests/JsonSchema/ObjectTypeTest.php
  • tests/JsonSchema/SerializerTest.php
  • tests/JsonSchema/StringTypeTest.php
  • tests/JsonSchema/TypeTest.php
  • tests/JsonSchema/UnionTypeTest.php

Comment thread src/json-schema/src/Types/AnyOfType.php
Reject nullable oneOf collapses when more than one branch can match null, including duplicate and reference-resolved null branches. Keep branch-local enums scoped inside nullable anyOf compositions and reject the corresponding oneOf form when flattening would change its meaning.

Add counterfactual coverage for exact oneOf cardinality, deliberate anyOf overlap, enum ownership, references, structural siblings, annotations, and existing conflict diagnostics. Clarify the supported reconstruction boundary in the canonical guide and keep the package README limited to genuine additive API differences.

Record the two upstream reconstruction defects under their durable audit findings. The checks remain bounded to explicit fromArray calls and add no shared state or request hot-path work.
@binaryfire

Copy link
Copy Markdown
Collaborator Author

@greptileai @coderabbitai Thanks for the review.

Fixed the nullable composition issue and the related enum ownership case:

  • Nullable oneOf now rejects duplicate or otherwise overlapping null matches, including reference-resolved branches.
  • Nullable anyOf preserves an enum owned by its non-null branch instead of hoisting it across the nullability boundary.
  • The equivalent oneOf form fails with a direct error when it cannot be represented faithfully.
  • The regression matrix covers direct and referenced branches, deliberate anyOf overlap, outer, equal, and conflicting enums, structural siblings, annotations, and malformed inputs.

I did not move empty-composition validation into the constructors. Empty construction is a supported intermediate for nullable-only schemas, and final serialization already rejects compositions that remain empty.

I also left the plan ordering unchanged. It explicitly distinguishes uncommitted implementation work from the later approval gate that forbids creating commits.

The canonical guide and audit records now describe the supported reconstruction boundary. The package README remains limited to genuine public API differences rather than listing framework bug fixes.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/json-schema/src/Deserializer.php --items all --type method --view expanded
ast-grep outline tests/JsonSchema/DeserializerTest.php --items all --type method --match 'Nullable|nullable|AnyOf|anyOf|OneOf|oneOf|Enum|enum|Reference|reference' --view expanded

printf '\n--- deserializer: composition/enum/reference locations ---\n'
rg -n -C 5 -i 'oneOf|anyOf|nullable|enum|reference|resolve' src/json-schema/src/Deserializer.php

printf '\n--- tests: composition/enum/reference locations ---\n'
rg -n -C 4 -i 'oneOf|anyOf|nullable|enum|reference' tests/JsonSchema/DeserializerTest.php

Length of output: 48495


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- nullable composition implementation ---'
sed -n '135,190p;636,780p' src/json-schema/src/Deserializer.php

printf '%s\n' '--- nullable composition regression tests ---'
sed -n '920,1225p' tests/JsonSchema/DeserializerTest.php

Length of output: 17455


@binaryfire Verified.

normalizeUnions() resolves local $ref branches before it checks null branches. It rejects duplicate bare null branches and non-bare branches that can accept null in nullable oneOf.

buildAnyOfComposition() preserves a non-null branch-local enum as AnyOfType. It does not move that enum across the nullability boundary.

The regression tests cover direct and referenced cases, enum ownership, conflicting enums, structural siblings, annotations, and malformed inputs.

Keeping empty compositions valid during construction is correct for nullable-only intermediate states. Final serialization remains the correct validation boundary.

🐇 ✅


✏️ Learnings added
Learnt from: binaryfire
URL: https://github.com/hypervel/components-backup/pull/1

Timestamp: 2026-08-08T14:15:43.743Z
Learning: In `src/json-schema`, empty `AnyOfType` and composition construction is supported as an intermediate state for nullable-only schemas. Final serialization, not the type constructors, rejects a composition that remains empty.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

@greptile-apps

greptile-apps Bot commented Aug 8, 2026

Copy link
Copy Markdown

The implementation handles all three concerns cleanly.

Duplicate null in oneOf — the $nullBranches > 1 guard in normalizeUnions fires before the single-branch check, so two bare {"type":"null"} branches fail with a direct message before reaching the collapse path. Reference-resolved null branches are caught because resolveRef runs on each branch before isNullBranch is evaluated.

mayAcceptNull coverage — returning true when type is absent is the right conservative stance; a branch with no type constraint matches everything including null, and failing loudly beats silently reconstructing a oneOf that the schema would never actually honour at one-of semantics.

Branch-local enum ownershiphasBranchOnlyEnumExcludingNull threads consistently through both paths: buildAnyOfComposition uses it to decide whether the nullable-collapse shortcut applies, and normalizeUnions uses it to produce the explicit oneOf error. The condition (!outer enum && branch has enum && null not in enum) correctly distinguishes the ownership boundary — a branch enum that already contains null is not a hoisting hazard, so that case is rightly excluded.

One minor observation for awareness rather than action: mayAcceptNull inspects only the direct type key, so a branch like {"anyOf": [{...}, {"type": "null"}]} would return true (no type key → may accept null → oneOf rejected). That's correct conservatism for the current scope, just worth documenting if the supported surface ever expands to nested compositions on the non-null branch.

The decision not to move empty-composition validation into constructors is sound. The nullable-only intermediate is a legitimate builder state, and serialization is already the right enforcement gate. Looks good overall.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

@binaryfire binaryfire closed this Aug 8, 2026
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.

1 participant