Skip to content

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

Closed

Conversation

wanlwanl
Copy link
Member

@wanlwanl wanlwanl commented Mar 4, 2025

Fix #5156 by using anyOf instead of allOf for unions, ao we can set diffirent types in anyOf
When std types are already defined, use ref to the scalar only.

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.

playground

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

Copy link
Contributor

@Copilot Copilot AI left a 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> {

@azure-sdk
Copy link
Collaborator

azure-sdk commented Mar 4, 2025

All changed packages have been documented.

  • @typespec/openapi3
Show changes

@typespec/openapi3 - fix ✏️

Fix union of custom scalar with null creating an object with allOf reference

@azure-sdk
Copy link
Collaborator

azure-sdk commented Mar 4, 2025

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

This reverts commit 40bde20.
This reverts commit 5beb68c.
const rootSchema = this.getSchemaForStdScalars(
currentRootScalar as unknown as Scalar & { name: IntrinsicScalarName },
);
return new ObjectBuilder({ ...rootSchema, ...additionalProps });
Copy link
Contributor

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

  1. use {} as null value in order to add $ref for better reference
  2. 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.

Copy link
Member

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.",
});
});

Copy link
Member

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)?

Copy link
Contributor

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.

playground

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also updated the description

Copy link
Member

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

Copy link
Member Author

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.

image

Copy link
Member Author

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 😢

Copy link
Member Author

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 },
],
});
});
Copy link
Member

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.

Copy link
Contributor

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
Copy link
Member Author

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

@chrisradek
Copy link
Member

@timotheeguerin and I looked through some more tooling/stackoverflow posts and from what we can gather, it seems at least many tools support setting nullable: true alongside allOf.

So for this PR, we can simplify things a lot. We can keep the current allOf behavior and just update the type so that for scalar types, type matches the scalar instead of object.

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.

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.

[Bug]: Nullable Generic Scalar generated as object
6 participants