Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed a bug where floats in the database would not be serialized corr… #89

Merged
merged 6 commits into from
Jul 26, 2023

Conversation

RemcoMeeuwissen
Copy link
Contributor

What I am changing

Ran into a bug where having a float in the database would cause a Type is not JSON serializable: decimal.Decimal error when requesting a json response.

How I did it

orjson has a default parameter when calling the dumps function which allows you to specify how to serialize unknown types, using this Decimal is now serialized to string.

Because the ORJsonResponse wrapper provided by fastapi does not support sending along this parameter I tweaked the GeoJSONResponse and SchemaJSONResponse classes to use orjson directly (basically recreating the wrapper, luckily it's fairly small).

How you can test it

I've added a number field to the my_data.sql fixture, this caused the bug to be triggered on existing tests so no new tests are needed.

content,
default=default,
option=orjson.OPT_NON_STR_KEYS | orjson.OPT_SERIALIZE_NUMPY,
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we create a custom JSONResponse class to avoid duplicating the render code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah should be easy

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 🙂

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. Can you check if we need to do the same thing for postgres fields of type "numeric" as well (which is equivalent to the postgres "decimal" type?

@vincentsarago
Copy link
Member

vincentsarago commented Jul 15, 2023

@RemcoMeeuwissen I've been away and focused on titiler for the past week, I'll try to have a look on all the contributions next week 🙏

def default(obj):
"""Instruct orjson what to do with types it does not natively serialize"""
if isinstance(obj, decimal.Decimal):
return str(obj)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we use string instead of float here?

The result of this will return a string within the JSON so we loose the fact that it is a number in the table

cc @RemcoMeeuwissen @bitner

Copy link
Contributor

@bitner bitner Jul 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not actually sure what we want here as best practice as decimal/numeric can be arbitrarily larger than what float could handle. Returning as numeric in the json would definitely fit most people's expectations, but could lose range / precision that is why people use those type rather than float.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah string felt like the safe option to me so that's why I went with it, but I don't know at which point precision is lost so I don't know if it is actually a relevant concern.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say that we stick with string as it is the only way we can be certain to not be lossy. If someone knows their data fits into a float, they can alter their table.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at simplejson library, it seems they do convert decimal to float

a = {"a": decimal.Decimal('1.00000000006')}

simplejson.dumps(a)
'{"a": 1.00000000006}'

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 I think it's because, simplejson will automatically convert decimal to float internally!

I'd say that we stick with string as it is the only way we can be certain to not be lossy. If someone knows their data fits into a float, they can alter their table.

👍 let's merge this, and we will revisit later if needed 🙏

INSERT INTO "public"."my_data" ("geom" , "id", "datetime") VALUES ('0103000020E610000001000000110000000A4C8422590E46C0B656FB86F03B5340D5E76A2BF60F46C0075F984C153C5340FA28B2217F0346C0CE0A257ADB3D5340BEE6287052F545C01AA33BF2DF3F5340F25A937BB7D244C009CB92853C69534049A5CD2EAE0644C03857A7D84686534063B4EEABC4F943C08D992E511D88534034A2B437F8EA43C0F54A5986388A53409C72BC6BC5E843C0920AAB5C038A534050AF9465883342C0363B85F6B5605340D43E0032881142C02A5884BF7F5D5340F4FDD478E90641C007F01648504453409C6F1F2DEA1541C00EA6095E6A4253404E4E9C88873342C06DC6E4C7471E53403EDF52396E3443C0DC9EAF2DC7FD524044696FF0854143C032772D211FFC52400A4C8422590E46C0B656FB86F03B5340', '4', '2004-10-23 10:23:54');
INSERT INTO "public"."my_data" ("geom" , "id", "datetime") VALUES ('0103000020E6100000010000000D000000BBE9944235C347C0EBF06E7961EE52406ADE718A8EC447C0D122DBF97EEE5240942D6301ECB947C05B59871F60F0524086CAEEF61AAE47C0BDEF3BBB76F252400A4C8422590E46C0B656FB86F03B5340FA28B2217F0346C0CE0A257ADB3D534057EC2FBB27F745C02B1895D409405340BEE6287052F545C01AA33BF2DF3F53401D386744692743C07958A835CDFF52403EDF52396E3443C0DC9EAF2DC7FD5240B9E39237FD0645C0574B4E2543B552400AD7A3703D1245C03A234A7B83B35240BBE9944235C347C0EBF06E7961EE5240', '5', '2004-10-24 10:23:54');
ALTER TABLE "public"."my_data" ADD COLUMN "decimal" DECIMAL;
ALTER TABLE "public"."my_data" ADD COLUMN "numeric" NUMERIC;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added values for numeric and it seems that they get returned as Decimal so we're fine

@@ -769,6 +769,10 @@ def test_items_env_table_config_main(app, monkeypatch):
body = response.json()
assert body["features"][0]["geometry"]["type"] == "Polygon"

# Make sure that Postgres Decimal and Numeric are converted to str
assert isinstance(body["features"][0]["properties"]["decimal"], str)
assert isinstance(body["features"][0]["properties"]["numeric"], str)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBD ☝️

Copy link
Contributor

@bitner bitner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is good to go.

@vincentsarago vincentsarago merged commit 09c9a8e into developmentseed:main Jul 26, 2023
7 checks passed
@@ -14,6 +14,7 @@ Note: Minor version `0.X.0` update might break the API, It's recommended to pin
- `type` query parameter to filter collections based on their type (`Function` or `Table`)
- fixed a small bug in the `tipg_properties` SQL function where the bounds property was not properly transformed to 4326 (author @RemcoMeeuwissen, https://github.com/developmentseed/tipg/pull/87)
- handling functions that are interpreted as collections but lack parameters (author @jackharrhy, https://github.com/developmentseed/tipg/pull/96)
- fixed a bug where orjson could not properly serialize float values present in the properties of a feature (author @RemcoMeeuwissen, https://github.com/developmentseed/tipg/pull/89)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll update this line to mention that Decimal will be converted to string

@RemcoMeeuwissen RemcoMeeuwissen deleted the fix-decimal-error branch December 13, 2023 12:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants