-
Notifications
You must be signed in to change notification settings - Fork 292
Fix the bug of nullable scalar generated as object #6240
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix the bug of nullable scalar generated as object #6240
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR Overview
This PR fixes the bug where a nullable scalar type is incorrectly generated as an object by replacing the use of allOf with anyOf for unions and refactoring the handling of scalar and model types.
- Updated the test cases to expect a scalar (i.e. "string") for nullable scalars
- Refactored the schema emitter to split the handling of model and scalar types and introduced a helper function to collect standard scalar names
- Updated the changelog to document the fix
Reviewed Changes
File | Description |
---|---|
packages/openapi3/test/union-schema.test.ts | Updated expected schema for unions using nullable types |
packages/openapi3/src/schema-emitter-3-0.ts | Refactored emitter logic for proper handling of scalar and model types and added a helper function for scalar info |
.chronus/changes/wanl-pr-fix-allof-object-2025-2-4-12-52-38.md | Added changelog entry documenting the bug fix |
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
Comments suppressed due to low confidence (2)
packages/openapi3/src/schema-emitter-3-0.ts:298
- [nitpick] Consider moving the ScalarMember interface to a module-level definition instead of defining it within a function, which can improve readability and reusability.
interface ScalarMember {
packages/openapi3/src/schema-emitter-3-0.ts:303
- [nitpick] The function name 'collectScalarInfo' could be more specific (e.g. 'collectStdScalarNames') to accurately convey that it returns a set of standard scalar names.
function collectScalarInfo(scalarMembers: ScalarMember[]): Set<string> {
All changed packages have been documented.
Show changes
|
You can try these changes here
|
const rootSchema = this.getSchemaForStdScalars( | ||
currentRootScalar as unknown as Scalar & { name: IntrinsicScalarName }, | ||
); | ||
return new ObjectBuilder({ ...rootSchema, ...additionalProps }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since we need to choose between
- use
{}
as null value in order to add$ref
for better reference - or add
nullable
and inline the std scalar schema
I implement the first option to safe time due to different time zone. Please feel free to leave comments if you want to change to other options.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i believe we agreed on 2. {}
means anything. I think the best result for any consuming tool for scalar is to inline as they are most likely to do that anyway.
@@ -682,6 +681,82 @@ worksFor(["3.0.0"], ({ diagnoseOpenApiFor, oapiForModel, openApiFor }) => { | |||
message: "Cannot have a union containing only null types.", | |||
}); | |||
}); | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so i am still confused by openapi3 nullable and allOf relation, does it also mean we technically have the same issue for models(are scalar not the only problematic ones)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The root cause is type: object
. Before this PR, we add type: object
to scalars like Str | null
, but it should be type: string
. But models is already type: object
, so it's not impacted.
openapi: 3.0.0
info:
title: Widget Service
version: 0.0.0
tags: []
paths: {}
components:
schemas:
Prop:
type: object # <------------- already an object
required:
- a
properties:
a:
type: string
Str:
type: string
Test:
type: object
required:
- a
- b
properties:
a:
type: object # <-------------------- should be string
allOf:
- $ref: '#/components/schemas/Str'
nullable: true
b:
type: object
allOf:
- $ref: '#/components/schemas/Prop'
nullable: true
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also updated the description
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah I get the original issue, but this has opened a can of worms.
How in openapi3 those 2 different?
schemas:
str:
type: string
MyModel:
type: object
properties: {...}
//--- why do we allow b but not a
Other:
a:
type: string
allOf:
- $ref: '#/components/schemas/str'
nullable: true
b:
type: object
allOf:
- $ref: '#/components/schemas/MyModel'
nullable: true
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, you are right, thanks for pointing out. I tested playground.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like if we don't use {not: anyOf: [...allTypes]}
, we have to duplicate model schema with nullable: true
😢
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like we can focus on scalar and create another pr to solve model issues
{ type: "string", nullable: true, minimum: 1 }, | ||
], | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think need @chrisradek also to do a final review on this as he's more knowledgable on the issue. He's OOF mon-wed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, thanks
|
||
const nullableProperty = nullable === true ? true : undefined; | ||
const innerSchema = this.getSchemaForScalar(type); | ||
// TODO: for the same constraints, use intersection of constraints from union wide and union member |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if inline, we got the same issue: #6368
@timotheeguerin and I looked through some more tooling/stackoverflow posts and from what we can gather, it seems at least many tools support setting So for this PR, we can simplify things a lot. We can keep the current So you could end up with: scalar testString extends string;
model Test {
field: testString | null;
} generating: Test:
type: object
required:
- field
properties:
field:
type: string
allOf:
- $ref: '#/components/schemas/testString'
nullable: true This won't work for all tooling, but matches the behavior we currently have for nullable objects. In the future if we're asked to support other outputs (e.g. inlined-schemas) we can support that via a flag. |
Fix #5156 by using
anyOf
instead ofallOf
for unions, ao we can set diffirent types inanyOf
When std types are already defined, use ref to the scalar only.
The root cause is
type: object
. Before this PR, we addtype: object
to scalars likeStr | null
, but it should betype: string
. But models is alreadytype: object
, so it's not impacted.playground