flask-restx icon indicating copy to clipboard operation
flask-restx copied to clipboard

Wildcard field contains an extra nested level in docs

Open anthiras opened this issue 5 years ago • 9 comments

This might be related to the recent wildcard field changes, e.g. #24.

When using a wildcard field with nested models, the resulting documentation contains three levels: *, <*>, and the nested model, where I would expect just two: The key <*> and the nested model. Example values in the doc also contains an extra nested level.

Please note, the actual output of the marshaled model in get is correct, only the swagger doc is wrong.

Code

from flask import Flask
from flask_restx import Resource, Api, fields

app = Flask(__name__)
api = Api(app)

nested_model = api.model('Nested model', {'nested_string': fields.String()})
wild = fields.Wildcard(fields.Nested(nested_model))
wildcard_model = api.model('Wildcard model', {'*': wild})

@api.route('/hello')
class HelloWorld(Resource):
    @api.marshal_with(wildcard_model)
    def get(self):
        return {
            'foo': {
                'nested_string': 'foo'
            },
            'bar': {
                'nested_string': 'bar'
            }
        }

if __name__ == '__main__':
    app.run(debug=True)

Repro Steps (if applicable)

  1. Run example above
  2. View documentation at http://localhost:5000

Actual model doc

wildcard-model

Expected model doc

Only one level of *, not two.

Actual example value doc

{
  "*": {
    "additionalProp1": {
      "nested_string": "string"
    },
    "additionalProp2": {
      "nested_string": "string"
    },
    "additionalProp3": {
      "nested_string": "string"
    }
  }
}

Expected example value doc

{
  "additionalProp1": {
    "nested_string": "string"
  },
  "additionalProp2": {
    "nested_string": "string"
  },
  "additionalProp3": {
    "nested_string": "string"
  }
}

or

{
  "*": {
    "nested_string": "string"
  }
}

Actual output of get (correct)

{
  "bar": {
    "nested_string": "bar"
  },
  "foo": {
    "nested_string": "foo"
  }
}

Environment

  • Python 3.7
  • Flask 1.1.1
  • Flask-RESTX 0.1.1

anthiras avatar Feb 14 '20 09:02 anthiras

Hello,

I'm not sure this is related to #24. The "double-nested" level is kind of expected (or at least it's how Wildcard fields have been initially implemented).

Here is the initial PR that introduced this feature: https://github.com/noirbizarre/flask-restplus/pull/255#issuecomment-392480891

As you can see, I already questioned about the resulting swagger, but at that time, nobody reacted to this limitation.

@SteadBytes, @j5awry do you think we could improve the Wildcard representation in the swagger?

ziirish avatar Feb 17 '20 10:02 ziirish

Hello @ziirish , When you said "kind of expected", I'm not sure if you mean that wildcards weren't meant for @anthiras' usecase or not. I have the same issue where I'd like to have:

{
  "field1": "value1",
  "nested_and_dynamic_keys": {
    "wildcard_field1": {
      "foo": "bar",
      "alice": "bob"
    },
    "wildcard_field2": {
      "foo": "rba",
      "alice": "bbo"
    }
  }
}

But I got this instead:

{
  "field1": "value1",
  "nested_and_dynamic_keys": {
    "*": {
      "wildcard_field1": {
        "foo": "bar",
        "alice": "bob"
      },
      "wildcard_field2": {
        "foo": "rba",
        "alice": "bbo"
      }
    }
  }
}

Is there a way to get the first result or is it really a bugfix/new feature?

Gaasmann avatar Feb 26 '20 21:02 Gaasmann

I also wanted this. I could get it to display how I wanted by taking out the Nested model, and having the wildcard field being referenced directly - but this functionally was wrong. So I ended up just being okay with the display looking looking a little off. Nevertheless if this is resolved that would be cool! :)

for me, model definition:

from flask import Flask
from flask_restx import Resource, Api, fields

app = Flask(__name__)
api = Api(app)

wild = {"*": fields.Wildcard(fields.String())}
final_model = api.model(
    'Final model', {"field1": fields.String(), "nested_and_dynamic_keys": fields.Nested(wild)}
)

@api.route('/hello')
class HelloWorld(Resource):
    @api.marshal_with(final_model)
    def get(self):
        return {
            'foo': {
                'nested_string': 'foo'
            },
            'bar': {
                'nested_string': 'bar'
            }
        }

if __name__ == '__main__':
    app.run(debug=True)

desired output:

{
  "field1": "value1",
  "nested_and_dynamic_keys": {
      "wildcard_field1": "foo"
      "wildcard_field2": "bar"
  }
}

actual output:

{
  "field1": "value1",
  "nested_and_dynamic_keys": {
    "*": {
    "wildcard_field1": "foo"
    "wildcard_field2": "bar"
  }
  }
}

esztermarton avatar Feb 27 '20 20:02 esztermarton

When you said "kind of expected", I'm not sure if you mean that wildcards weren't meant for @anthiras' usecase or not.

Actually, I didn't find yet a swagger definition that matches a model with wildcard fields. That's the reason why the generated schema may look strange.

ziirish avatar Mar 03 '20 22:03 ziirish

I would expect a wildcard to be a pattern. https://swagger.io/specification/#schemaObject In the yaml style:

      username:
        type: string
        pattern: "[a-z0-9]{8,64}"
        minLength: 8
        maxLength: 64

So a wildcard in a nested field would be:

{
    "field1": "value1",
    "nested_and_dynamic_keys": {
        "variable_name": {
            "pattern": "regex",
         },
         "variable_name": {
            "pattern": "regex"
         }
    }
}

I don't have access to some of the examples I made in the past at the moment...and sadly won't for roughly a month. I'll try and mock this up

j5awry avatar Mar 06 '20 07:03 j5awry

@ziirish Here is an example of swagger definition for wildcard :

swagger: '2.0'
paths:
  '/ipam/{subnet}-{mask}':
    get:
      responses:
        '200':
          description: OK
          schema:
            $ref: '#/definitions/document%20download%20response'
        '400':
          description: Failed. Bad post data.
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
      description: Request the number of available IPv4 by subnet/mask
      operationId: get_add
      parameters:
        - name: subnet
          in: path
          required: true
          type: string
          description: 'IP Network address ex: 192.168.10.0'
        - name: mask
          in: path
          required: true
          type: string
          description: 'Mask bits ex: 24'
      tags:
        - ipam
info:
  title: Network and Security REST API
  version: v0.7.3
  description: Network and Security REST API
  contact:
    name: Florian Lacommare
produces:
  - application/json
consumes:
  - application/json
definitions:
  document download response:
    properties:
      free_ip_number:
        type: string
        description: Number of IPv4 available in a subnet.
      free_ip_percent:
        type: string
        description: Percentage of IP addresses available in a subnet.
      ip_list:
        description: Dictionary with ips as key and properties of the ip as value.
        type: object
        additionalProperties:
          $ref: '#/definitions/IP'
        example: 
          ip1:
            dns_name: test
            alias: test
            type: test
            requester: test
            description: test
            environment: test
          ip2:
            dns_name: test
            alias: test
            type: test
            requester: test
            description: test
            environment: test
    type: object
  IP:
    properties:
      dns_name:
        type: string
        description: Domain name
        example: frsvp.com
      alias:
        type: string
        description: alias
        example: test1.com
      type:
        type: string
        description: Type of equipment
      requester:
        type: string
        description: The firstname_lastname of the requester
      description:
        type: string
        description: 'The description of your IP (server name, project, etc)'
      environment:
        type: string
        description: Environment type
    type: object

The example output show in Swagger :

{
  "free_ip_number": "string",
  "free_ip_percent": "string",
  "ip_list": {
    "ip1": {
      "dns_name": "test",
      "alias": "test",
      "type": "test",
      "requester": "test",
      "description": "test",
      "environment": "test"
    },
    "ip2": {
      "dns_name": "test",
      "alias": "test",
      "type": "test",
      "requester": "test",
      "description": "test",
      "environment": "test"
    }
  }
}

image

FloLaco avatar Jul 21 '20 16:07 FloLaco

It seems like wildcards can't live in the root level of the object with api.model()... Wish it would be added soon...

syzhakov avatar Oct 01 '20 16:10 syzhakov

@ziirish I suffer from this behavior as well and in my opinion this is clearly a bug as @FloLaco showed. Shall I make a PR which "fixes" this? Maybe with a new default pre-set parameter like nested=True for backwards compatibility reasons? Or is this intended behavior and a "feature" and no "fix" is welcomed?

Nantero1 avatar Apr 20 '21 16:04 Nantero1

Is there some solution / workaround for this issue?

lennart-bader avatar Apr 29 '24 14:04 lennart-bader