hey-api / openapi-ts

✨ Turn your OpenAPI specification into a beautiful TypeScript client
https://heyapi.vercel.app
Other
963 stars 77 forks source link

AdditionalProperties renders as `unknown` #773

Closed MichaelCereda closed 3 weeks ago

MichaelCereda commented 1 month ago

Description

I have an api written as follows

...
responses:
        '200':
          description: |
            <description>
          content:
            application/json:
              schema:
                type: object
                allOf:
                  - $ref: '#/components/schemas/PaginationResponse'
                  - $ref: '#/components/schemas/SortingResult'
                  - type: object
                    required:
                      - entries
                    properties:
                      entries:
                        type: object
                        additionalProperties:
                          $ref: '#/components/schemas/ProductExcerpt'
...

When rendered the output is as follows:

export type SearchOrganizationProductsResponse = PaginationResponse & SortingResult & {
    /**
     * List of products
     *
     */
    entries: unknown;
};

I would expect an output like the following instead

export type SearchOrganizationProductsResponse = PaginationResponse & SortingResult & {
    /**
     * List of products
     *
     */
    entries: Record<string, ProductExcerpt>;
};

Reproducible example or configuration

openapi-ts --input ./.tmp/output.yaml --output ./ts-client --client fetch --useOptions --name MyApi

I also updated my configuration to

import { defineConfig } from '@hey-api/openapi-ts';

export default defineConfig({
  input: './.tmp/output.yaml',
  output: 'ts-client',
  client:'@hey-api/client-fetch',
  types: {
     enums: 'typescript',
     export: true

  },

});

OpenAPI specification (optional)

...
responses:
        '200':
          description: |
            <description>
          content:
            application/json:
              schema:
                type: object
                allOf:
                  - $ref: '#/components/schemas/PaginationResponse'
                  - $ref: '#/components/schemas/SortingResult'
                  - type: object
                    required:
                      - entries
                    properties:
                      entries:
                        type: object
                        additionalProperties:
                          $ref: '#/components/schemas/ProductExcerpt'
...

System information (optional)

No response

mrlubos commented 1 month ago

@MichaelCereda which version are you on? I am unable to reproduce. Please add a reproducible example if I need to investigate further

MichaelCereda commented 1 month ago

@mrlubos sorry for the delay. here's my reproducible example

package.json

{
  "name": "heyapi",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "openapi": "npx @hey-api/openapi-ts",
    "redocly": "redocly bundle sample.yaml --output dist/sample.yaml"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@hey-api/openapi-ts": "^0.50.1"
  }
}

openapi-ts.config.mjs

import { defineConfig } from '@hey-api/openapi-ts';

export default defineConfig({
  input: './dist/sample.yaml',
  output: 'ts-client',
  client:'@hey-api/client-fetch',
  types: {
     enums: 'typescript',
     export: true,

  },

});

dist/sample.yaml

openapi: 3.1.0
info:
  title: mytest API
  description: mytest customer facing API
  termsOfService: https://mytest.com/terms
  contact:
    name: mytest
    email: help@mytest.com
    url: https://mytest.com
  version: 1.0.0
  license:
    name: Apache 2.0
    url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
  - url: https://api.mytest.com
security:
  - JWTAuth: []
  - ApiKeyAuth: []
tags:
  - name: products
    description: products
paths:
  /organizations/{org_ern}/products/search:
    post:
      operationId: searchOrganizationProducts
      summary: Search for products
      tags:
        - products
      parameters:
        - $ref: '#/components/parameters/org_ern'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HubSearchRequest'
      responses:
        '200':
          description: |
            Lists the products matching the search criteria
          content:
            application/json:
              schema:
                type: object
                allOf:
                  - $ref: '#/components/schemas/PaginationResponse'
                  - $ref: '#/components/schemas/SortingResult'
                  - type: object
                    required:
                      - entries
                    properties:
                      entries:
                        description: |
                          List of products
                        additionalProperties:
                          $ref: '#/components/schemas/ResourceReference'
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorMessage'
components:
  securitySchemes:
    JWTAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-KEY
  schemas:
    ErnField:
      type: string
      pattern: ^[a-z]{3}:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
      description: |
        External id of a resource, this is guaranteed to be unique across all the resources of an organization.
    ErrorMessage:
      type: object
      required:
        - message
        - status
      properties:
        message:
          type: string
        status:
          type: string
    PaginationResponse:
      type: object
      properties:
        pagination:
          type: object
          properties:
            chunk:
              type: string
            prev:
              type: string
            next:
              type: string
    SortingResult:
      type: object
      required:
        - result
      properties:
        result:
          type: array
          items:
            type: string
    ResourceReference:
      type: object
      required:
        - name
        - ern
      properties:
        ern:
          $ref: '#/components/schemas/ErnField'
        name:
          type: string
    HubSearchRequest:
      type: object
      properties:
        category_name:
          type: string
        domain:
          type: string
        category_erns:
          type: array
          items:
            type: string
        company_name:
          type: string
        product_name:
          type: string
        compliance_framework_erns:
          type: array
          items:
            type: string
  parameters:
    org_ern:
      name: org_ern
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/ErnField'
      description: Organization's unique identifier

Now I run the command

$ npm run openapi

that results in

image

services.gen.ts contains

// This file is auto-generated by @hey-api/openapi-ts

import { client, type Options } from '@hey-api/client-fetch';
import type { SearchOrganizationProductsData, SearchOrganizationProductsError, SearchOrganizationProductsResponse } from './types.gen';

/**
 * Search for products
 */
export const searchOrganizationProducts = (options: Options<SearchOrganizationProductsData>) => { return (options?.client ?? client).post<SearchOrganizationProductsResponse, SearchOrganizationProductsError>({
    ...options,
    url: '/organizations/{org_ern}/products/search'
}); };

types.gen.ts contains

// This file is auto-generated by @hey-api/openapi-ts

/**
 * External id of a resource, this is guaranteed to be unique across all the resources of an organization.
 *
 */
export type ErnField = string;

export type ErrorMessage = {
    message: string;
    status: string;
};

export type PaginationResponse = {
    pagination?: {
        chunk?: string;
        prev?: string;
        next?: string;
    };
};

export type SortingResult = {
    result: Array<(string)>;
};

export type ResourceReference = {
    ern: ErnField;
    name: string;
};

export type HubSearchRequest = {
    category_name?: string;
    domain?: string;
    category_erns?: Array<(string)>;
    company_name?: string;
    product_name?: string;
    compliance_framework_erns?: Array<(string)>;
};

/**
 * Organization's unique identifier
 */
export type Parameterorg_ern = ErnField;

export type SearchOrganizationProductsData = {
    body: HubSearchRequest;
    path: {
        /**
         * Organization's unique identifier
         */
        org_ern: ErnField;
    };
};

export type SearchOrganizationProductsResponse = PaginationResponse & SortingResult & {
    /**
     * List of products
     *
     */
    entries: unknown;  /// <---- this is what my issue is all about
};

export type SearchOrganizationProductsError = ErrorMessage;

export type $OpenApiTs = {
    '/organizations/{org_ern}/products/search': {
        post: {
            req: SearchOrganizationProductsData;
            res: {
                /**
                 * Lists the products matching the search criteria
                 *
                 */
                '200': PaginationResponse & SortingResult & {
    /**
     * List of products
     *
     */
    entries: unknown;
};
                /**
                 * Invalid request
                 */
                '400': ErrorMessage;
            };
        };
    };
};

Please note the line

export type SearchOrganizationProductsResponse = PaginationResponse & SortingResult & {
    /**
     * List of products
     *
     */
    entries: unknown;  /// <---- this is what my issue is all about
};

I hope this helps. Thank you

qqilihq commented 3 weeks ago

Just been hit by this as well - here's an even more minimalized example spec:

openapi: 3.1.0

info:
  version: 1.0.0

components:
  schemas:
    MySchema:
      type: object
      properties:
        foo:
          additionalProperties:
            anyOf:
              - type: string
              - type: number
          type: object
          properties: {}

… which currently produces …

export type MySchema = {
    foo?: {
        [key: string]: (unknown) | undefined; // TODO : should be `string | number | undefined`
    };
};

I might try to come up with a fix MR if the time before holidays allows.

mrlubos commented 3 weeks ago

Finally got around to this. Thanks for your patience here!

qqilihq commented 3 weeks ago

That's great - thank you!

MichaelCereda commented 3 weeks ago

Fantastic! I will test it out very soon