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

How to create a dictionary type in an API model that has arbitrary field names?

Open espears1 opened this issue 4 years ago • 5 comments

I have several use cases where the stakeholders making requests to my webservice require an API component that is essentially an arbitrary dictionary but with some extra type information about the keys and values.

For example, in one case, my stakeholder needs to present an input as a dictionary with string keys and string values, but the actual names of the keys will be dynamic depending on user data. This stakeholder specifically needs a structure that looks like

{string_key: string_value}  
# where string_key is a completely arbitrary string at request time

and specifically does not look like

{'key': string_key, 'value': string_value}

or any similar variation that has fixed field names. This is for critical performance reasons related to directly indexing by 'string_key' into a hash table and avoiding a linear scan of the data to locate the item where the index at 'key' has the matching value.

This can be sort of solved by using fields.Raw() to allow an arbitrary JSON object for this component, but this renders the swagger API spec useless as it just says 'object' with no type structure at all and makes it much harder to automatically enforce the correct string types for keys and values on entry. It also makes things really hard for stakeholders who generate clients in other programming languages from the swagger spec, particularly in statically typed OO languages where having a field as an unrestricted JSON object causes type safety and request-correctness enforcement problems.

fields.Wildcard is also unfortunately no use because it cannot be part of the compiled Model object, but in my use case I need wildcard-like behavior on field names specifically to be part of the compiled Model object that renders the API spec. The use case doesn't require any marshaling outside of the Model.

How can I solve this problem with flask-restx and design and compile an input component definition that allows arbitrary field names but has proper API specifications for the field's data type and the value's data type?

espears1 avatar Apr 15 '20 14:04 espears1

Hi @espears1 ,

I also needed that feature to dynamically serve key/value data from a dict to the client. My solution involves a new basic type DictItem:

from flask_restx import fields
class DictItem(fields.Raw):
    def output(self, key, obj, *args, **kwargs):
        try:
            dct = getattr(obj, self.attribute)
        except AttributeError:
            return {}
        return dct or {}

task_model = api.model(
    "Task",
    {
        "id": fields.String(readOnly=True),
        "calling_args": DictItem(attribute="calling_args"),
    },
)

The result from the api then looks like (simply empty in case of an empty dict):

[
  {
    "id": "task1",
    "calling_args": {
      "a": 1,
      "b": 2
    }
  },
  {
    "id": "task2",
    "calling_args": {
      
    }
  }
]

While the generated swagger api renders the schema like: image

Hope this helps !

mafeko avatar May 18 '20 09:05 mafeko

@mafeko thank you for the example. That is very cool, but unfortunately does not cover the use case.

In my case, I would need your example calling_args to have some additional fixed structure to it within the published API. For example, the keys and values to calling_args need to be enforceable as type fields.String (but their values can be any valid string).

Essentially I need something of type Dict[str, str] enforced in the API. If I can't enforce structure on the type allowed for the keys and values, it would be no benefit beyond fields.Raw to also document that it is some type of dictionary.

espears1 avatar May 23 '20 20:05 espears1

I also needed that feature to dynamically serve key/value data from a dict to the client. My solution involves a new basic type DictItem

Exact solution I was looking for. Dev's should implement this.

synchronizing avatar Jun 30 '20 04:06 synchronizing

It is semi-implemented via the Wildcard type, but the implementation is not very easy to use and overly complicated, and generates incorrect swagger documentation

Mause avatar Jul 06 '20 11:07 Mause

I experience the same regarding the Wildcards.

Marshaling either fails with an error - while Wildcarded model has correct swagger documentation - or marshaling works fine but then it really generates a faulty swagger doc.

I'm afraid I have to live with the latter for now.

martincpt avatar Mar 16 '23 13:03 martincpt