Skip to content

fix: nested schema output, custom keywords, and named properties - #34

Merged
tymondesigns merged 5 commits into
mainfrom
cursor/json-schema-nested-schema-fixes-dfaf
Sep 15, 2026
Merged

tymondesigns merged 5 commits into
mainfrom
cursor/json-schema-nested-schema-fixes-dfaf

Conversation

@tymondesigns

@tymondesigns tymondesigns commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Fixes several schema-output bugs and fills gaps in the object/keyword APIs. Existing properties(), required(), property(), requireProperties(), and requireAll() behaviour is preserved.

Nested items were invalid JSON Schema

Array item schemas were serialised as if they were document roots. That leaked a nested $schema (not allowed under JSON Schema 2020-12 unless the subschema is a resource root) and a redundant title. Nested items now match tupleItems, additionalItems, and properties: no dialect URI, no leftover title.

Numeric property names restated themselves as title, and numeric-only maps encoded as arrays

PHP coerces array keys like "0" and "1" to integers. Title-vs-name de-duplication compared those keys strictly, so a property named "0" kept "title": "0" in the output. Comparison and getPropertyKeys() now treat names as strings.

A map of only numeric names was also a PHP list, so toJson() emitted "properties":[...]. JSON Schema requires an object; list-shaped maps are now cast before encoding.

Empty examples lists were published

examples([]) wrote "examples": []. Empty lists are now omitted; non-empty examples() is unchanged.

Untitled nested properties had no usable API

properties() still requires each child to have a title (that title is the property name). There was no way to name a property independently of title, including anonymous nested objects.

This now uses property() from #33. The key is the property name; title stays metadata; untitled nested schemas are allowed. Child required() is honoured as well as the required: flag.

Schema::object('user')
    ->property('name', Schema::string()->required())
    ->property('nested', Schema::object())
    ->property('email', Schema::string()->title('Email Address'));

When properties() still rejects an untitled child, the error now names the parent, 1-based argument position, and offending class, e.g. Property 2 of "Wrapper" (Cortex\JsonSchema\Types\ObjectSchema) must have a title. Named arguments (first:, second:) keep string keys in PHP, so the index is taken from argument order rather than the key name.

No escape hatch for unmodelled keywords

Draft keywords we do not model, OpenAPI discriminator, and x-* extensions could not be attached without a special schema subclass. keyword($name, $value) is now available on every schema, including nested ones. Nested JsonSchema values serialise without $schema.

Schema::object('Pet')
    ->keyword('discriminator', [
        'propertyName' => 'petType',
    ])
    ->keyword('x-additionalPropertiesName', 'attributes');

Runtime required lists needed requireAll() or per-child required()

There was no counterpart to requireAll() for a computed list of names (and those names did not have to exist yet). That is requireProperties() from #33:

Schema::object('user')
    ->property('name', Schema::string())
    ->property('email', Schema::string())
    ->requireProperties(...$model::requiredFields());

requireAll() now uses stored property keys, so it works for untitled schemas added via property().

Docs

Documented that repeated properties() / property() calls merge (union); the last schema for a given name wins. Custom keywords are listed as an additional feature; contains() remains in the overview.

Open in Web Open in Cursor 

@tymondesigns
tymondesigns marked this pull request as ready for review September 15, 2026 22:00
Reuse property()/requireProperties() from #33 instead of adding
propertyMap()/require(). Nested items omit $schema; numeric-string
property keys compare as strings; empty examples are omitted; keyword()
covers unmodelled/vendor keywords.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
@cursor
cursor Bot force-pushed the cursor/json-schema-nested-schema-fixes-dfaf branch from 8870c1e to c8590f0 Compare September 15, 2026 22:09
cursoragent and others added 2 commits September 15, 2026 22:13
Annotate mixed schema arrays in the JS-* Pest cases and apply Pest
coding-style rector so format:check stays clean on PHP 8.4.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
covers(AbstractSchema::class) made Pest mutation run only
KeywordSchemaTest against AbstractSchema, dropping the suite below
the 80% mutation floor. Point covers at the trait under test instead.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
@cursor cursor Bot changed the title fix: JS-1–JS-8 nested schema output, keywords, and property APIs fix: nested schema output, custom keywords, and named properties Sep 15, 2026
@tymondesigns
tymondesigns requested a balanced review from Copilot September 15, 2026 22:22

Copilot AI 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.

🟡 Changes recommended

Numeric-only property names still serialize invalidly, and the public interface change introduces an undocumented compatibility break.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Fixes nested schema serialization and expands object/property and custom-keyword APIs.

Changes:

  • Removes invalid nested $schema, redundant titles, and empty examples.
  • Adds custom keywords and named/required property support.
  • Adds documentation and regression tests.
File summaries
File Description
src/Contracts/JsonSchema.php Adds the custom keyword contract.
src/Types/AbstractSchema.php Integrates custom keyword serialization.
src/Types/Concerns/HasItems.php Corrects nested item serialization.
src/Types/Concerns/HasKeywords.php Implements arbitrary keyword support.
src/Types/Concerns/HasMetadata.php Omits empty examples.
src/Types/Concerns/HasProperties.php Expands named and required property handling.
tests/Unit/Types/ArraySchemaTest.php Tests nested item output.
tests/Unit/Types/KeywordSchemaTest.php Tests custom keywords.
tests/Unit/Types/ObjectSchemaTest.php Tests property API changes.
tests/Unit/Types/StringSchemaTest.php Tests empty examples.
tests/Unit/Converters/ClassConverterTest.php Updates nested schema expectations.
tests/Unit/Converters/ClosureConverterTest.php Updates nested schema expectations.
docs/json-schema/introduction.mdx Documents new behavior.
docs/json-schema/schema-types/object.mdx Documents object property APIs.
Review details
  • Files reviewed: 14/14 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Types/Concerns/HasProperties.php Outdated
Comment thread src/Contracts/JsonSchema.php
PHP stores "0"/"1" keys as ints, so json_encode emitted a properties
array. Cast list-shaped maps to objects so the output stays valid
JSON Schema.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>

Copilot AI 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.

🔵 Needs a closer look

Named variadic property arguments produce incorrect diagnostic indexes, and the feature overview drops supported contains validation.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

src/Types/Concerns/HasProperties.php:67

  • Named arguments passed to a variadic parameter retain their string keys, so properties(first: ..., second: $untitled) casts second to 0 and incorrectly reports Property 1. Reindex the arguments before iterating so the new diagnostic always uses the actual 1-based position.
    docs/json-schema/introduction.mdx:146
  • This replaces the existing “Contains Validation” entry even though contains() remains supported and documented. Keep that feature in the overview and add custom keywords as a separate bullet.
  • Files reviewed: 14/14 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Untitled-property errors used the variadic array key as the 1-based
index, so named arguments reported Property 1. Reindex before iterating.
Keep Contains Validation in the introduction overview alongside Custom Keywords.

Co-authored-by: Sean Tymon <tymondesigns@users.noreply.github.com>
@tymondesigns
tymondesigns merged commit 9d328c2 into main Sep 15, 2026
14 checks passed
@tymondesigns
tymondesigns deleted the cursor/json-schema-nested-schema-fixes-dfaf branch September 15, 2026 22:38
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.

3 participants