flask-restx
flask-restx copied to clipboard
How to create a dictionary type in an API model that has arbitrary field names?
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?
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:
Hope this helps !
@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.
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.
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
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.