Repository navigation
Description
Summary
For a model whose only extra constraint is a declared indexer — i.e. a plain dictionary such as
Record<string> — the OpenAPI 3.1 emitter writes the indexer as unevaluatedProperties. That is
defensible as JSON Schema, but in practice essentially no TypeScript client generator implements
unevaluatedProperties, so the field is read as having no permitted properties: a dictionary
becomes Record<string, never>, an object that must be empty.
additionalProperties expresses the same constraint for that shape and is universally supported.
Repro
model HealthReport {
status: "ok";
checks: Record<string>;
}Emitted (openapi-versions: ["3.1.0"]):
"checks": {
"type": "object",
"unevaluatedProperties": { "type": "string" }
}Expected:
"checks": {
"type": "object",
"additionalProperties": { "type": "string" }
}Downstream, a generated TypeScript client types checks as an object with no permitted properties,
so writing report.checks["db"] = "ok" is a type error and reading any key yields never. The
service returns a perfectly good document; only the generated client is wrong.
Where
packages/openapi3/src/schema-emitter-3-1.ts, the indexer branch of the 3.1 schema emitter
(dist/src/schema-emitter-3-1.js around the shouldSealSchema check in 1.15.0 and 1.16.0, both
identical here):
const unevaluatedPropertiesSchema = shouldSeal
? { not: {} }
: this.emitter.emitTypeReference(model.indexer.value);
setProperty(schema, "unevaluatedProperties", unevaluatedPropertiesSchema);The keyword is chosen unconditionally, so the sealing case and the declared-indexer case get the
same one.
Suggested fix
Choose the keyword by case rather than emitting one for both. unevaluatedProperties is genuinely
required when the model is sealed (not: {}) and when it has a base model, because there
the properties to account for are evaluated by an allOf subschema that additionalProperties
cannot see. A leaf dictionary has neither, and additionalProperties is correct and portable:
const indexerKeyword =
shouldSeal || model.baseModel ? "unevaluatedProperties" : "additionalProperties";
setProperty(schema, indexerKeyword, indexerSchema);This is not hypothetical — it is the patch we and others apply to ship. It has been carried across
1.15.0 and 1.16.0 unchanged, and is republished per compiler release, which is why it would be
better upstream than in every consumer.
Why not an existing option
seal-object-schemas governs sealing (defaulting closed models to additionalProperties: false /
unevaluatedProperties: false). It does not affect which keyword a declared indexer emits, and
turning it on would be wrong here anyway — the model is a dictionary, not a sealed object. As far as
I can find there is no emitter option covering this; #3241 and #3549 are related but both address
sealing/configuration rather than the indexer keyword.
Environment
@typespec/openapi3 |
1.15.0 and 1.16.0 (identical in this region) |
@typespec/compiler |
1.16.0 |
openapi-versions |
["3.1.0"] |
Activity
github-actions commentedon Sep 13, 2026
🎯 Issue Triage
The OpenAPI 3.1 emitter uses
unevaluatedPropertiesfor a leaf model's declaredRecordindexer, causing client generators without support for that keyword to interpret the dictionary as having no valid properties. For this shape,additionalPropertiesprovides equivalent constraints with broader tooling compatibility.Category: Bug
Area:
emitter:openapi3🔗 [Playground Reproduction]((typespec.io/redacted)
📋 Reproduction code
🔍 Analysis
This affects
@typespec/openapi3when targeting OpenAPI 3.1, specifically the indexer branch inpackages/openapi3/src/schema-emitter-3-1.ts. The emitter currently selectsunevaluatedPropertiesunconditionally for both sealing and declared-indexer cases.unevaluatedPropertiesremains necessary when sealing a schema or accounting for properties evaluated through a base model'sallOf, but a leaf model with a declared indexer can useadditionalProperties. Issues #3241 and #3549 are related to sealing/configuration but do not track this keyword-selection bug.📝 Suggested next steps
Recordindexer, an inherited indexed model, and a sealed model.additionalPropertiesfor the leaf declared-indexer case while retainingunevaluatedPropertieswhereallOfor sealing semantics require it.bimsonz commentedon Sep 13, 2026
Closing this — I filed it prematurely and it was not mine to file.
On reflection I am not confident this is a defect in TypeSpec at all.
unevaluatedPropertiesis valid JSON Schema and arguably the more precise keyword for a declared indexer; the complaint is really about client-generator support, which is a different argument. It may well be that the right home for any change is the downstream emitter, where it currently sits.It has been routed to the team that owns our TypeSpec work. If they judge it worth raising they will do so properly scoped. Apologies for the noise.
[openapi3] emit additionalProperties for a declared Record indexer in…