7

I have code that looks something like this using Fast API:

class EnumTestT(Enum):
    test_t_value = 0

object = { 
    test: test_t_value
}

enum_mapping = {
    test_t_value: "Test"
}

def enum_encoder(val: EnumTestT) -> str:
    return enum_mapping[val]

custom_encoder = {
    EnumTestT: enum_encoder
}

@app.get("/test")
async def test_get():
    return jsonable_encoder(object, custom_encoder=custom_encoder)

The issue is that jsonable_encoder applies custom encoders after the defaults. Is there any way to apply them before the default encoders. Because for an Enum and any derived classes the value of the enum is reported instead of the mapped value.

Karlson
  • 2,958
  • 1
  • 21
  • 48
  • Do you mean https://fastapi.tiangolo.com/advanced/response-directly/ ? – lsabi Jun 10 '20 at 20:26
  • @lsabi Not sure I understand the question. – Karlson Jun 10 '20 at 20:50
  • I mean if you need something as shown in the link. You can return a response that you prepare on your own and that does not go through the jsonable_encoder – lsabi Jun 10 '20 at 20:54
  • @lsabi I have a dict which I have represented simplistically in the code. The object is more complex and need to be encoded. So either I write a completely custom encoder by myself, which I would like to avoid or use `jsonable_encoder` but one thing that I can't seem to do is to provide a mnemonic instead of a numerical value for an enum. – Karlson Jun 10 '20 at 21:01
  • Yes, but if you try to serialize your object with json.dumps(my_object) does it return the desired result? In case you can return that result directly, instead of passing through the jsonable_encoder of fastapi – lsabi Jun 10 '20 at 21:09
  • @lsabi Not without a custom parser. Native `json` module doesn't process Enums directly – Karlson Jun 10 '20 at 21:17
  • Then I guess it is not possible, as the docs of fastapi explain _The result of calling it is something that can be encoded with the Python standard json.dumps()_ (https://fastapi.tiangolo.com/advanced/response-directly/) – lsabi Jun 10 '20 at 21:23
  • could this help ? https://stackoverflow.com/questions/43854335/encoding-python-enum-to-json – lsabi Jun 10 '20 at 21:41
  • I've already figured it out how to use `json.dumps()` with custom encoder, but the issue is about FastAPI. – Karlson Jun 10 '20 at 21:55
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/215728/discussion-between-lsabi-and-karlson). – lsabi Jun 11 '20 at 09:05
  • Do you find any solutions? – Yagiz Degirmenci Jul 16 '20 at 17:16

5 Answers5

5

FastAPI use ENCODERS_BY_TYPE (from pydantic.json) to encode some basic data type.

ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
    bytes: lambda o: o.decode(),
    Color: str,
    datetime.date: isoformat,
    datetime.datetime: isoformat,
    datetime.time: isoformat,
    datetime.timedelta: lambda td: td.total_seconds(),
    Decimal: decimal_encoder,
    Enum: lambda o: o.value,

so for me to override the default datetime encode, just like

 ENCODERS_BY_TYPE[datetime] = lambda date_obj: date_obj.strftime("%Y-%m-%d %H:%M:%S")

coder vx
  • 61
  • 1
  • 3
4

Right now I am using custom encoder within json.dumps like this:

class TestEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, EnumTestT):
            return TestEncoder.process_enum_test_t(obj)

and response within the FastAPI app would be:

json_str = json.dumps(json_list, cls=TestEncoder).encode('utf-8')
return Response(media_type="application/json", content=json_str)

The problem with FastAPI using custom encoders is that custom encoder is invoked after all the standard encoders have been invoked and there is no way to override that order.

Karlson
  • 2,958
  • 1
  • 21
  • 48
2

inspired by coder vx

import datetime
from fastapi.responses import JSONResponse 
...
content = something to serialize 
JSONResponse(jsonable_encoder(content,
    custom_encoder={datetime.datetime: lambda date_obj: 
    date_obj.strftime("%Y-%m-%d %H:%M:%S")})))
Alan.I
  • 21
  • 1
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Dec 31 '21 at 10:26
1

Taking this one step further, you could probably subclass FastAPI/Starlette's JSONResponse (see implementation of JSONResponse) with your custom encoder:

import json
from json import JSONEncoder
from fastapi.responses import JSONResponse


class MyJSONEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, UUID):
            return str(obj)
        elif isinstance(obj, datetime):
            return obj.strftime('%Y-%m-%dT%H:%M:%SZ')
        else:
            return super().default(obj)


class MyJSONResponse(JSONResponse):
    def render(self, content: Any) -> bytes:
        return json.dumps(
            content,
            ensure_ascii=False,
            cls=MyJSONEncoder,
            allow_nan=False,
            indent=None,
            separators=(",", ":"),
        ).encode("utf-8")

Then in your route you can return MyJSONResponse(some_dict_obj). Note: json doesn't support UUIDs as keys, so you'll need to find a way to convert them to int or str.

Chris
  • 18,724
  • 6
  • 46
  • 80
Curt
  • 149
  • 1
  • 4
0

You can use any serializer by returning a fastapi.responses.Response like so:

from fastapi.responses import Response

return Response(
    content=CustomJsonEncoder().to_string(result),
    media_type='application/json'
)

by writing a class that inherits from json.JSONEncoder you can override the method def default(self, obj) to change the serialization for specific data types.

class CustomJsonEncoder(json.JSONEncoder):

    def default(self, obj):
        if isinstance(obj, specific_class_type):
            return specific_serialization(obj)

    pass
Danny Varod
  • 17,324
  • 5
  • 69
  • 111