core icon indicating copy to clipboard operation
core copied to clipboard

[OpenAPI] non-nullable properties is possibly undefined in typescript (missing required field?)

Open Richard87 opened this issue 2 years ago • 2 comments

API Platform version(s) affected: 2.6.8

Description
OpenAPI schema definition is missing the required field, causing all types generated to be possible undefined:

This is an one of the objects generated, with a few nullable types:

"Indicator": {
    "type": "object",
    "description": "",
    "properties": {
        "id": {
            "readOnly": true,
            "type": "integer"
        },
        "order": {
            "type": "integer"
        },
        "group": {
            "type": "string",
            "format": "iri-reference",
            "nullable": true
        },
        "title": {
            "type": "string"
        },
        "remoteId": {
            "readOnly": true,
            "type": "string",
            "nullable": true
        },
        "createdAt": {
            "readOnly": true,
            "type": "string",
            "format": "date-time"
        },
        "disabled": {
            "readOnly": true,
            "type": "boolean"
        },
        "version": {
            "readOnly": true,
            "type": "integer"
        },
        "optional": {
            "type": "boolean"
        },
        "description": {
            "type": "string"
        },
        "answers": {
            "writeOnly": true,
            "type": "array",
            "items": {
                "$ref": "#/components/schemas/AbstractAnswer"
            }
        },
        "dependencies": {
            "type": "array",
            "items": {
                "type": "string",
                "format": "iri-reference"
            }
        },
        "relianceOn": {
            "readOnly": true,
            "type": "array",
            "items": {
                "type": "string",
                "format": "iri-reference"
            }
        },
        "benchmarks": {
            "writeOnly": true,
            "type": "array",
            "items": {
                "$ref": "#/components/schemas/Benchmark"
            }
        },
        "form": {
            "readOnly": true,
            "type": "string",
            "format": "iri-reference",
            "nullable": true
        },
        "public": {
            "type": "boolean"
        },
        "copyFunction": {
            "type": "boolean"
        },
        "dependencyMethods": {
            "readOnly": true,
            "type": "array",
            "items": {
                "type": "string"
            }
        },
        "dependency": {
            "writeOnly": true,
            "$ref": "#/components/schemas/Indicator"
        },
        "subType": {
            "readOnly": true,
            "type": "string"
        }
    }
},

Notice that only group, form and remoteId is nullable.

openapi-typescript sets all fields as possible undefined, with the proper optional null types:

interface  Indicator {
      id?: number;
      order?: number;
      /** Format: iri-reference */
      group?: string | null;
      title?: string;
      remoteId?: string | null;
      /** Format: date-time */
      createdAt?: string;
      disabled?: boolean;
      version?: number;
      optional?: boolean;
      description?: string;
      answers?: components["schemas"]["AbstractAnswer"][];
      dependencies?: string[];
      relianceOn?: string[];
      benchmarks?: components["schemas"]["Benchmark"][];
      /** Format: iri-reference */
      form?: string | null;
      public?: boolean;
      copyFunction?: boolean;
      dependencyMethods?: string[];
      dependency?: components["schemas"]["Indicator"];
      subType?: string;
    };

How to reproduce
Inspect the output from this command: npx openapi-typescript http://localhost:8000/api/docs.json?spec_version=3 --output assets/Generated/api.ts

Additional Context
OpenAPI's specification includes the required field in the examples: (ref. https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#model-with-example)

in: query
name: coordinates
content:
  application/json:
    schema:
      type: object
      required:
        - lat
        - long
      properties:
        lat:
          type: number
        long:
          type: number

Also a different related discussion about the problem: https://github.com/OpenAPITools/openapi-generator/issues/7565

Richard87 avatar Apr 01 '22 06:04 Richard87

I wrote a OpenApiFactory to modify the type, that "works", but I dont know enough about OpenAPI / API-Platform to verify it, or if it will have any unforseen side-effects...

class OpenApiFactory implements OpenApiFactoryInterface
{
    public function __construct(
        private OpenApiFactoryInterface $decorated
    ) {
    }

    public function __invoke(array $context = []): OpenApi
    {
        $api = $this->decorated->__invoke($context);

        /** @var \ArrayObject[] $schemas */
        $schemas = $api->getComponents()->getSchemas();
        foreach ($schemas as $schema) {
            $properties = $schema['properties'] ?? [];
            $required = $schema['required'] ?? [];

            foreach ($properties as $field => $property) {
                $type = $property['type'] ?? false;
                $nullable = $property['nullable'] ?? false;

                if ($type && !$nullable && !in_array($field, $required)) {
                    $required[] = $field;
                }
            }

            $schema['required'] = $required;
        }

        return $api;
    }
}

This is the end result (generated typescript types):

interface Indicator: {
      id: number;
      order: number;
      /** Format: iri-reference */
      group?: string | null;
      title: string;
      remoteId?: string | null;
      /** Format: date-time */
      createdAt: string;
      disabled: boolean;
      version: number;
      optional: boolean;
      description: string;
      answers: components["schemas"]["AbstractAnswer"][];
      dependencies: string[];
      relianceOn: string[];
      benchmarks: components["schemas"]["Benchmark"][];
      /** Format: iri-reference */
      form?: string | null;
      public: boolean;
      copyFunction: boolean;
      dependencyMethods: string[];
      dependency?: components["schemas"]["Indicator"];
      subType: string;
    };

Richard87 avatar Apr 01 '22 06:04 Richard87

Thank you for the report, according to the JSON schema specification required must always be an array

The value of this keyword MUST be an array. This array MUST have at least one element. Elements of this array MUST be strings, and MUST be unique. An object instance is valid against this keyword if its property set contains all elements in this keyword's array value.

source

This needs to be fixed in the SchemaFactory.

soyuka avatar Apr 05 '22 09:04 soyuka

Any progress on this? I have to add exclamation mark to access any properties :(

Pearseak avatar Nov 17 '22 08:11 Pearseak

@Pearseak checked your profile, do you also have back-end in ASP.NET? If that is true, you can consider using https://github.com/reinforced/Reinforced.Typings. I used it in my last project, and in worked well, though I still hope someone fix this issue, becuase its crutial for my https://github.com/MadL1me/aspnet-awesome-templates repo

MadL1me avatar Nov 26 '22 19:11 MadL1me

So, what's the deal here? Everything can be undefined, why is it so?

fredowashere avatar Jun 05 '23 13:06 fredowashere

This bug is targeting 2.7, as API Platform 3.2 is out, version 2.7 has reached end of life. Therefore we'll close this issue.

We recommend to upgrade to API Platform 3.0, Les-Tilleuls.coop can offer paid support to help or even migrate your projects if they have tests.

We want to fund a Long Term Stable version of API Platform, if you or your organization would like to contribute to LTS support, please visit our Open Collective crowdfunding.

soyuka avatar Oct 17 '23 09:10 soyuka

So we investigated and null and required are two different things, also detecting this would be a mistake from our perspective. You can use #[ApiProperty(required: true)] if you want.

soyuka avatar Oct 27 '23 10:10 soyuka

I am encountering the same issue where all read operations from the API define all fields as possibly undefined, e.g., name?: string. When using a TypeScript generator, in my case, the official one from https://openapistack.co/docs/openapicmd/typegen/, I always have to check whether the property exists. While the issue can be addressed by using the #[ApiProperty(required: true)] attribute, it becomes very tedious to check each of the thousands of possible fields individually.

I believe a good compromise would be to declare properties as required by default if the field is not nullable in the database. For example:

#[ORM\Table]
class Address
{
    #[ORM\Column]
    private string $name;

    #[ORM\ManyToOne(targetEntity: User::class)]
    #[ORM\JoinColumn(referencedColumnName: 'uuid', nullable: false)]
    private User $user;
}

In this case, we can be quite certain that both properties will be present in the response, as long as the normalization group is marked. Whereas if the fields are optional for the database, they could continue to appear as possibly undefined.

#[ORM\Table]
class Address
{
    #[ORM\Column(nullable: true)]
    private null|string $name = null;

    #[ORM\ManyToOne(targetEntity: User::class)]
    #[ORM\JoinColumn(referencedColumnName: 'uuid', nullable: true)]
    private null|User $user = null;
}

This approach would significantly reduce the manual checking required.

DpVic avatar Feb 28 '24 18:02 DpVic