Skip to content

feat: support schemas combining 'properties' and 'additionalProperties'#141

Merged
martin-helmich merged 4 commits into
masterfrom
feat/additional-properties
Jul 8, 2026
Merged

feat: support schemas combining 'properties' and 'additionalProperties'#141
martin-helmich merged 4 commits into
masterfrom
feat/additional-properties

Conversation

@martin-helmich

Copy link
Copy Markdown
Owner

Summary

Schemas that declare both properties and additionalProperties were previously rejected with a GeneratorException ("using 'properties' and 'additionalProperties' in the same schema is currently not supported"). This broke code generation for the mittwald mStudio API, whose spec now contains such a schema (de.mittwald.v1.domainmigration.DomainNotMigratableValidationError's context property).

Such schemas are now generated as regular classes in which all declared properties work as before, while undeclared input properties are collected into an $additionalProperties map:

  • buildFromInput() maps undeclared input keys into the map, converting each value according to the additionalProperties schema (including $validate passthrough for object-typed values)
  • toJson() serializes the map back out; it is written before the declared properties, so declared properties win on key collisions
  • getAdditionalProperties() / withAdditionalProperties() expose the map
  • __clone() deep-clones map values where the value type requires it
  • object-typed additionalProperties values get their own generated class ({Class}AdditionalPropertiesItem)

Implementation notes

  • The invariant check in PropertyBuilder is removed; NestedObjectProperty now claims schemas that have declared properties even when additionalProperties is also present. Purely map-like schemas (only additionalProperties) are still handled by the array property types, so their behavior is unchanged.
  • The value type of additionalProperties is modeled as a pseudo-property built via PropertyBuilder, so all existing mapping/serialization/clone expression machinery is reused.
  • Schemas that declare a regular property literally named additionalProperties while also using schema-level additionalProperties still throw, since the generated member would collide.
  • Two new snapshot fixtures cover the string-valued and object-valued variants.

🤖 Generated with Claude Code

martin-helmich and others added 4 commits July 8, 2026 10:50
Schemas that declare both 'properties' and 'additionalProperties' were
previously rejected with a GeneratorException. Such schemas are now
generated as regular classes in which all declared properties work as
before, and undeclared input properties are collected into an
additional-properties map:

- buildFromInput() maps undeclared input keys into the map, converting
  each value according to the 'additionalProperties' schema
- toJson() serializes the map back, with declared properties taking
  precedence on key collisions
- getAdditionalProperties()/withAdditionalProperties() expose the map
- __clone() deep-clones map values where necessary
- object-typed 'additionalProperties' values get their own generated
  class ({Class}AdditionalPropertiesItem)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NamespaceInferrer joined the working directory and a relative target
directory with PATH_SEPARATOR (':' on Unix), which broke namespace
inference for all relative paths, including 'generate:fromspec' with
the repository's own .s2c.yaml.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Optional properties with a schema default are declared non-nullable and
initialized with their default, so the isset() guards emitted around
their toJson() serialization and __clone() handling were always true.
PHPStan >= 2.2 rejects isset() on non-nullable properties
(isset.property), which broke CI on every regeneration of the example
classes.

The Spec classes are regenerated with the fixed generator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The additional-properties map is string-keyed, so plain 'T[]' was
imprecise; use PHPStan-style 'array<string, T>' in the property,
getter, and wither annotations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@martin-helmich martin-helmich merged commit 23036de into master Jul 8, 2026
2 checks passed
@martin-helmich martin-helmich deleted the feat/additional-properties branch July 8, 2026 09:23
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