Skip to content

Lower bare type: object to an open map - #1

Open
4t145 wants to merge 1 commit into
masterfrom
fix/bare-object-schemas
Open

Lower bare type: object to an open map#1
4t145 wants to merge 1 commit into
masterfrom
fix/bare-object-schemas

Conversation

@4t145

@4t145 4t145 commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary

A type: object schema with no declared properties was classified as Shape::Object and lowered through build_struct, emitting a fieldless pub struct Foo {}. That type drops every key on serialize and carries nothing on deserialize, so a client holding one has no way to send the real payload.

The array case is what surfaced this. Given:

references:
  type: array
  items:
    type: object

the generator hoisted pub struct XxxReferences {} and produced Vec<XxxReferences> — an array the caller can only fill with empty objects, silently discarding whatever they meant to send.

JSON Schema defaults additionalProperties to true, so a bare type: object in fact accepts any object. It is now classified as Shape::Map, which already lowers to BTreeMap<String, serde_json::Value>.

additionalProperties: false stays on the struct path: with no declared properties it genuinely describes the empty object and nothing else, so the fieldless struct is the faithful lowering.

Version bumped to 0.1.3, following the two prior codegen fixes.

Before / after

spec before after
type: object pub struct Opaque {} pub type Opaque = BTreeMap<String, Value>
type: object, additionalProperties: false pub struct Sentinel {} unchanged
type: object with properties pub struct User { .. } unchanged
array of items: {type: object} Vec<XxxItems> (empty struct) Vec<BTreeMap<String, Value>>
array of items: {} Vec<Value> unchanged

Test plan

  • cargo test -p toac-build -p toac — all 22 test targets pass, including the petstore 3.1 and e2b real-spec snapshot tests, so no existing lowering shifted
  • cargo clippy -p toac-build -p toac --all-targets — no warnings
  • cargo fmt — clean
  • New toac-build/tests/test_bare_object_schemas.rs, 4 cases: bare object → open map, array-of-bare-objects keeps the payload reachable, additionalProperties: false keeps its struct, object-with-properties as a control
  • Mutation-checked: reverting classify() fails the 3 behavioural cases and leaves the control passing

Not covered: no test asserts the generated code round-trips a real payload through serde. The assertions are on the emitted type, not on wire behaviour.

🤖 Generated with Claude Code

A `type: object` schema with no declared `properties` was classified as
`Shape::Object` and lowered through `build_struct`, emitting a fieldless
`pub struct Foo {}`. That type drops every key on serialize and carries
nothing on deserialize, so a client holding one has no way to send the
real payload — the array case is worse, since `items: {type: object}`
hoists a named `pub struct XxxItems {}` and the whole array becomes
unusable.

JSON Schema defaults `additionalProperties` to `true`, so such a schema
in fact accepts any object. Classify it as `Shape::Map` instead, which
already lowers to `BTreeMap<String, serde_json::Value>`.

`additionalProperties: false` stays on the struct path: with no declared
properties it genuinely describes the empty object and nothing else, so
the fieldless struct is the faithful lowering.

Adds regression tests for the bare object, the array-of-bare-objects
shape that surfaced this, the closed-empty exception, and an object with
properties as a control.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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