openapi-generator icon indicating copy to clipboard operation
openapi-generator copied to clipboard

[BUG] Error generating models with types referencing to attributes types

Open Sincasios opened this issue 3 years ago • 1 comments

Bug Report Checklist

  • [X] Have you provided a full/minimal spec to reproduce the issue?
  • [X] Have you validated the input using an OpenAPI validator (example)?

This API is valid, but it cannot be shown because it contains circular references

Yes, with openapi-generator-cli-7.0.0-20220719.043604-2.jar, valides says "OK" but generates crashes: Exception in thread "main" java.lang.RuntimeException: Could not process model 'EventFilter_exptUeBehav_expectedUmts_inner_geographicAreas_inner_anyOf_1'.Please make sure that your schema is correct!

  • [X] Have you searched for related issues/PRs?
  • [X] What's the actual output vs expected output?

A python-flask server that can be launched

  • [ ] [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

I'm triying to generate a OpenAPI server from 3GPP 5G specs. I got this specs from this repository https://github.com/jdegre/5GC_APIs and trying to build the .yaml called: TS29520_Nnwdaf_AnalyticsInfo.yaml

openapi-generator-cli generate -g python-flask -i TS29520_Nnwdaf_AnalyticsInfo.yaml -o output

Then I tried to install dependencies:

cd output
python3 -m venv .
bin/pip install -r requirements.txt
bin/python -m openapi_server

And got the first error: ImportError: cannot import name 'escape' from 'jinja2' (~/openapi/5GC_APIs/output/lib/python3.8/site-packages/jinja2/__init__.py)

I searched for this and is for the flask version (1.1.2): https://stackoverflow.com/questions/71718167/importerror-cannot-import-name-escape-from-jinja2 I updated the Flask version:

bin/pip install Flask==2.1.0

And now it starts with the generated models problems:

bin/python -m openapi_server
ImportError: cannot import name 'NnwdafEventsSubscription' from partially initialized module 'openapi_server.models.nnwdaf_events_subscription' (most likely due to a circular import) (~/openapi/5GC_APIs/output/openapi_server/models/nnwdaf_events_subscription.py)

Reading some bugs opened here I found "swagger-cli bundle", I tried this:

swagger-cli bundle -o b_TS29520_Nnwdaf_AnalyticsInfo.yaml -r -t yaml TS29520_Nnwdaf_AnalyticsInfo.yaml

But "dereference" doesn't work because there is a "Circular $ref pointer", but if I remove this I can generate a single .yaml with all the spec:

swagger-cli bundle -o b_TS29520_Nnwdaf_AnalyticsInfo.yaml -t yaml TS29520_Nnwdaf_AnalyticsInfo.yaml

I uploaded the output .yaml as gziped file (to use github upload): b_TS29520_Nnwdaf_AnalyticsInfo.yaml.gz

Now repeat the code generation using this new bundled yaml:

rm -rf output
openapi-generator-cli generate -g python-flask -i b_TS29520_Nnwdaf_AnalyticsInfo.yaml -o output
cd output
python3 -m venv .
bin/pip install -r requirements.txt
bin/pip install Flask==2.1.0
bin/python -m openapi_server

And now the real question comes: It raises new errors based on genereated models from the yaml:

from openapi_server.models.network_area import NetworkArea
ModuleNotFoundError: No module named 'openapi_server.models.network_area'

If we look the spec we can see where this come from some attributes referencing other types:

              networkArea:
                $ref: '#/components/schemas/EventFilter/properties/networkArea'

And this adds the "import openapi_server.models.network_area" but this file doesn't exists. The real module is:

models/event_filter_network_area.py

Found here:

$ find -iname "*network_area*"
./openapi_server/models/event_filter_network_area_g_ran_node_ids_inner_gnb_id.py
./openapi_server/models/event_filter_network_area_g_ran_node_ids_inner.py
./openapi_server/models/event_filter_network_area_ecgis_inner.py
./openapi_server/models/event_filter_network_area_ncgis_inner.py
./openapi_server/models/event_filter_network_area_tais_inner.py
./openapi_server/models/event_filter_network_area.py  <------------------------------------

So this is my first problem or bug: When a type references a subtype like "#/components/schemas/EventFilter/properties/networkArea" code adds a import to "openapi_server.models.network_area" but this doesn't exists, it is "models.event_filter_network_area import EventFilterNetworkArea"

I tried to fix this case with a string replace: Search for "models.network_area import Network area" and replace this to "model.event_filter_network_area import EventFilterNetworkArea as NetworkArea": sed -i "s/models.network_area import NetworkArea/models.event_filter_network_area import EventFilterNetworkArea as NetworkArea/g" *.py

And it worked... but now I have a second situation, similar but different:

    from openapi_server.models.start import Start
ModuleNotFoundError: No module named 'openapi_server.models.start'

This is more funny, because there isn't any "start" model. What happens here is initially the same: Types referencing other types.

                  startTs:
                    $ref: '#/components/schemas/AnalyticsData/properties/start'

And if we go to "AnalyticsData" we can see the definition as a string with format date-time:

      properties:
        start:
          format: date-time
          type: string
          description: string with format 'date-time' as defined in OpenAPI.

The problem here is that "AnalyticsData" doesn't generates any class to contain this "start" property, it keeps the datetime directly:

        :param start: The start of this AnalyticsData.  # noqa: E501
        :type start: datetime
        .....
        self.openapi_types = {
            'start': datetime,

So no "analytics_data_start" module is generated and nothing can be imported. The openapi-generator should 2 one of this things:

  • See that the referenced type is a basic type and inline it in all other models.
  • Always generate a "model.*" for all attributes (or al least all referenced attributes)

In the same "AnalyticsData" reference we have the two situations: the start that defines that is a string with "format: date-time" and then "expiry" that references to start to define his type:

        :param start: The start of this AnalyticsData.  # noqa: E501
        :type start: datetime <---- Inlined datetime
        :param expiry: The expiry of this AnalyticsData.  # noqa: E501
        :type expiry: Start <---- Using inexistent "model.start" instead of use "datetime"

Is this a bug? Is a openapi-generator limitation? Maybe a incorrect spec? I'm not and OpenAPI expert, but reading the yaml I can see that OpenAPI has the information to generate the code.

openapi-generator version
$ openapi-generator-cli version
6.0.1

Downloaded using npm.

OpenAPI declaration file content or url

b_TS29520_Nnwdaf_AnalyticsInfo.yaml.gz

Generation Details

openapi-generator-cli generate -g python-flask -i b_TS29520_Nnwdaf_AnalyticsInfo.yaml -o output

Steps to reproduce

Described in "Description", but basically:

openapi-generator-cli generate -g python-flask -i b_TS29520_Nnwdaf_AnalyticsInfo.yaml -o output
cd output
python3 -m venv .
bin/pip install -r requirements.txt
bin/pip install Flask==2.1.0
bin/python -m openapi_server
Suggest a fix

For the first case: Don't import the "last slash" (or generate this model). It is importing the inexistent "NetworkArea" (generatedfrom /NetworkArea) when the real import is EventFilterNetworkArea.

For the second case:

  • See that the referenced type is a basic type and inline it in all other models.
  • Always generate a "model.*" for all attributes (or al least all referenced attributes)

Sincasios avatar Aug 03 '22 11:08 Sincasios

I have a small OpenAPI yml with the 2 cases isolated:

openapi: 3.0.0
info:
  version: 1.0.0
  title: Error case
paths:
  /analytics:
    get:
      operationId: GetTest
      parameters:
        - name: TestObject
          in: query
          schema:
            $ref: '#/components/schemas/TestObject'
      responses:
        '200':
          description: OK

components:
  schemas:
    TestObject:
      description: Object with an attribute with type string - date-time + other attributes referencing it
      type: object
      properties:
        basictype:
          type: string
          description: A basic type

        composedType:
          description: The base composed type
          type: object
          properties:
            innertype:
              type: string
              description: The innertype

        refComposed:
          description: Reference to a composed field
          $ref: '#/components/schemas/TestObject/properties/composedType'

        refBasic:
          description: Referencing a basic type
          $ref: '#/components/schemas/TestObject/properties/basictype'

That I generate, prepare and execute with:

openapi-generator-cli generate -g python-flask -i error.yml -o output
cd output
python3 -m venv .
bin/pip install -r requirements.txt
bin/pip install Flask==2.1.0
bin/python -m openapi_server

The first problem is the basictype reference: ModuleNotFoundError: No module named 'openapi_server.models.basictype'

We can see in the code the import and the type mapping:

        self.openapi_types = {
            'basictype': str, <------------------- Base type
...
            'ref_basic': Basictype <----------- Referenced type, inexistent
        }

I can fix this removing imports and changing to 'str', now the problem is the reference to a composed subtype: ModuleNotFoundError: No module named 'openapi_server.models.composed_type'

Because the model is "test_object_composed_type" I can fix this in this test making a import rename: Remove: from openapi_server.models.composed_type import ComposedType

And add: from openapi_server.models.test_object_composed_type import TestObjectComposedType as ComposedType

And now the server works fine:

$ bin/python -m openapi_server
 * Serving Flask app '__main__' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on all addresses (0.0.0.0)
   WARNING: This is a development server. Do not use it in a production deployment.
 * Running on http://127.0.0.1:8080
 * Running on http://192.168.30.2:8080 (Press CTRL+C to quit)

I can patch the code of this small example, but for a real API like the provided by 3GPP is very tedious.

Sincasios avatar Aug 03 '22 14:08 Sincasios