diff --git a/datasette/views/base.py b/datasette/views/base.py index 6c69c538..0aff72bc 100644 --- a/datasette/views/base.py +++ b/datasette/views/base.py @@ -183,6 +183,15 @@ class BaseView(RenderMixin): forward_querystring=False, ) + # Handle the _json= parameter which may modify data["rows"] + json_cols = [] + if "_json" in request.args: + json_cols = request.args["_json"] + if json_cols and "rows" in data and "columns" in data: + data["rows"] = convert_specific_columns_to_json( + data["rows"], data["columns"], json_cols, + ) + # Deal with the _shape option shape = request.args.get("_shape", "arrays") if shape == "arrayfirst": @@ -323,3 +332,22 @@ class BaseView(RenderMixin): "canned_query": canned_query, "config": self.ds.config, }, templates + + +def convert_specific_columns_to_json(rows, columns, json_cols): + json_cols = set(json_cols) + if not json_cols.intersection(columns): + return rows + new_rows = [] + for row in rows: + new_row = [] + for value, column in zip(row, columns): + if column in json_cols: + try: + value = json.loads(value) + except (TypeError, ValueError) as e: + print(e) + pass + new_row.append(value) + new_rows.append(new_row) + return new_rows diff --git a/docs/json_api.rst b/docs/json_api.rst index 4164cc20..379186a1 100644 --- a/docs/json_api.rst +++ b/docs/json_api.rst @@ -131,6 +131,33 @@ this format. The ``object`` keys are always strings. If your table has a compound primary key, the ``object`` keys will be a comma-separated string. +Special JSON arguments +---------------------- + +Every Datasette endpoint that can return JSON also accepts the following +querystring arguments: + +``?_shape=SHAPE`` + The shape of the JSON to return, documented above. + +``?_json=COLUMN1&_json=COLUMN2`` + If any of your SQLite columns contain JSON values, you can use one or more + ``_json=`` parameters to request that those columns be returned as regular + JSON. Without this argument those columns will be returned as JSON objects + that have been double-encoded into a JSON string value. + + Compare `this query without the argument `_ to `this query using the argument `_ + +``?_timelimit=MS`` + Sets a custom time limit for the query in ms. You can use this for optimistic + queries where you would like Datasette to give up if the query takes too + long, for example if you want to implement autocomplete search but only if + it can be executed in less than 10ms. + +``?_ttl=SECONDS`` + For how many seconds should this response be cached by HTTP proxies? Use + ``?_ttl=0`` to disable HTTP caching entirely for this request. + Special table arguments ----------------------- @@ -163,16 +190,6 @@ The Datasette table view takes a number of special querystring arguments: You can pass multiple ``_group_count`` columns to return counts against unique combinations of those columns. -``?_timelimit=MS`` - Sets a custom time limit for the query in ms. You can use this for optimistic - queries where you would like Datasette to give up if the query takes too - long, for example if you want to implement autocomplete search but only if - it can be executed in less than 10ms. - -``?_ttl=SECONDS`` - For how many seconds should this response be cached by HTTP proxies? Use - ``?_ttl=0`` to disable HTTP caching entirely for this request. - ``?_next=TOKEN`` Pagination by continuation token - pass the token that was returned in the ``"next"`` property by the previous page. diff --git a/tests/test_api.py b/tests/test_api.py index 455ff864..05929342 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1151,3 +1151,39 @@ def test_suggest_facets_off(): def test_ttl_parameter(app_client, path, expected_cache_control): response = app_client.get(path, gather_request=False) assert expected_cache_control == response.headers['Cache-Control'] + + +test_json_columns_default_expected = [{ + "intval": 1, + "strval": "s", + "floatval": 0.5, + "jsonval": "{\"foo\": \"bar\"}" +}] + + +@pytest.mark.parametrize("extra_args,expected", [ + ("", test_json_columns_default_expected), + ("&_json=intval", test_json_columns_default_expected), + ("&_json=strval", test_json_columns_default_expected), + ("&_json=floatval", test_json_columns_default_expected), + ("&_json=jsonval", [{ + "intval": 1, + "strval": "s", + "floatval": 0.5, + "jsonval": { + "foo": "bar" + } + }]) +]) +def test_json_columns(app_client, extra_args, expected): + sql = ''' + select 1 as intval, "s" as strval, 0.5 as floatval, + '{"foo": "bar"}' as jsonval + ''' + path = "/test_tables.json?" + urllib.parse.urlencode({ + "sql": sql, + "_shape": "array" + }) + path += extra_args + response = app_client.get(path, gather_request=False) + assert expected == response.json