From 7a74e4d6d04ae224efa3b4c2aa2f68140046f86a Mon Sep 17 00:00:00 2001 From: Egor Mikhnevich Date: Wed, 17 Sep 2025 17:48:21 +0200 Subject: [PATCH 1/6] Use the built-in JSON library by default --- config/config.exs | 2 +- installer/templates/phx_single/config/config.exs | 4 ++-- installer/templates/phx_umbrella/config/extra_config.exs | 4 ++-- installer/test/phx_new_test.exs | 2 +- installer/test/phx_new_umbrella_test.exs | 2 +- integration_test/config/config.exs | 2 +- lib/phoenix.ex | 4 +++- test/phoenix/socket/v1_json_serializer_test.exs | 6 +++--- test/phoenix/socket/v2_json_serializer_test.exs | 2 +- test/phoenix/test/conn_test.exs | 6 +++--- 10 files changed, 18 insertions(+), 16 deletions(-) diff --git a/config/config.exs b/config/config.exs index 9656d8e51a..69f5ee1176 100644 --- a/config/config.exs +++ b/config/config.exs @@ -5,7 +5,7 @@ config :logger, :console, format: "\n$time $metadata[$level] $message\n" config :phoenix, - json_library: Jason, + json_library: JSON, stacktrace_depth: 20, trim_on_html_eex_engine: false diff --git a/installer/templates/phx_single/config/config.exs b/installer/templates/phx_single/config/config.exs index 11496cdbc4..a15fcba6f3 100644 --- a/installer/templates/phx_single/config/config.exs +++ b/installer/templates/phx_single/config/config.exs @@ -59,8 +59,8 @@ config :logger, :default_formatter, format: "$time $metadata[$level] $message\n", metadata: [:request_id] -# Use Jason for JSON parsing in Phoenix -config :phoenix, :json_library, Jason +# Use built-in `JSON` for JSON parsing in Phoenix +config :phoenix, :json_library, JSON # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. diff --git a/installer/templates/phx_umbrella/config/extra_config.exs b/installer/templates/phx_umbrella/config/extra_config.exs index 713d9b6bc4..9af3ece5fa 100644 --- a/installer/templates/phx_umbrella/config/extra_config.exs +++ b/installer/templates/phx_umbrella/config/extra_config.exs @@ -5,8 +5,8 @@ config :logger, :default_formatter, format: "$time $metadata[$level] $message\n", metadata: [:request_id] -# Use Jason for JSON parsing in Phoenix -config :phoenix, :json_library, Jason +# Use built-in `JSON` for JSON parsing in Phoenix +config :phoenix, :json_library, JSON # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs index c1c1166f62..f97068a17c 100644 --- a/installer/test/phx_new_test.exs +++ b/installer/test/phx_new_test.exs @@ -54,7 +54,7 @@ defmodule Mix.Tasks.Phx.NewTest do assert_file("phx_blog/config/config.exs", fn file -> assert file =~ "ecto_repos: [PhxBlog.Repo]" assert file =~ "generators: [timestamp_type: :utc_datetime]" - assert file =~ "config :phoenix, :json_library, Jason" + assert file =~ "config :phoenix, :json_library, JSON" assert file =~ ~s[cd: Path.expand("../assets", __DIR__),] refute file =~ "namespace: PhxBlog" refute file =~ "config :phx_blog, :generators" diff --git a/installer/test/phx_new_umbrella_test.exs b/installer/test/phx_new_umbrella_test.exs index 61258f8cbe..aaa8cbb70d 100644 --- a/installer/test/phx_new_umbrella_test.exs +++ b/installer/test/phx_new_umbrella_test.exs @@ -63,7 +63,7 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do assert file =~ ~r/config :esbuild/ assert file =~ "cd: Path.expand(\"../apps/phx_umb_web/assets\", __DIR__)" assert file =~ ~S[import_config "#{config_env()}.exs"] - assert file =~ "config :phoenix, :json_library, Jason" + assert file =~ "config :phoenix, :json_library, JSON" assert file =~ "ecto_repos: [PhxUmb.Repo]" assert file =~ ":phx_umb_web, PhxUmbWeb.Endpoint" assert file =~ "generators: [context_app: :phx_umb]\n" diff --git a/integration_test/config/config.exs b/integration_test/config/config.exs index a596f1c7d3..541288954f 100644 --- a/integration_test/config/config.exs +++ b/integration_test/config/config.exs @@ -1,6 +1,6 @@ import Config -config :phoenix, :json_library, Jason +config :phoenix, :json_library, JSON config :swoosh, api_client: false diff --git a/lib/phoenix.ex b/lib/phoenix.ex index 9a4d5b7ac3..bcc5226508 100644 --- a/lib/phoenix.ex +++ b/lib/phoenix.ex @@ -6,6 +6,8 @@ defmodule Phoenix do """ use Application + @default_json_library if Code.ensure_loaded?(JSON), do: JSON, else: Jason + @doc false def start(_type, _args) do # Warm up caches @@ -45,7 +47,7 @@ defmodule Phoenix do """ def json_library do - Application.get_env(:phoenix, :json_library, Jason) + Application.get_env(:phoenix, :json_library, @default_json_library) end @doc """ diff --git a/test/phoenix/socket/v1_json_serializer_test.exs b/test/phoenix/socket/v1_json_serializer_test.exs index 62652d3f26..00943d556d 100644 --- a/test/phoenix/socket/v1_json_serializer_test.exs +++ b/test/phoenix/socket/v1_json_serializer_test.exs @@ -24,7 +24,7 @@ defmodule Phoenix.Socket.V1.JSONSerializerTest do msg = %Message{topic: "t", event: "e", payload: "m"} encoded = encode!(@serializer, msg) - assert Jason.decode!(encoded) == %{ + assert JSON.decode!(encoded) == %{ "event" => "e", "payload" => "m", "ref" => nil, @@ -36,7 +36,7 @@ defmodule Phoenix.Socket.V1.JSONSerializerTest do msg = %Reply{topic: "t", ref: "null"} encoded = encode!(@serializer, msg) - assert Jason.decode!(encoded) == %{ + assert JSON.decode!(encoded) == %{ "event" => "phx_reply", "payload" => %{"response" => nil, "status" => nil}, "ref" => "null", @@ -61,7 +61,7 @@ defmodule Phoenix.Socket.V1.JSONSerializerTest do msg = %Broadcast{topic: "t", event: "e", payload: "m"} encoded = fastlane!(@serializer, msg) - assert Jason.decode!(encoded) == %{ + assert JSON.decode!(encoded) == %{ "event" => "e", "payload" => "m", "ref" => nil, diff --git a/test/phoenix/socket/v2_json_serializer_test.exs b/test/phoenix/socket/v2_json_serializer_test.exs index c534ff47d3..25b1d4505b 100644 --- a/test/phoenix/socket/v2_json_serializer_test.exs +++ b/test/phoenix/socket/v2_json_serializer_test.exs @@ -102,7 +102,7 @@ defmodule Phoenix.Socket.V2.JSONSerializerTest do msg = %Reply{topic: "t", payload: %{m: 1}} encoded = encode!(@serializer, msg) - assert Jason.decode!(encoded) == [ + assert JSON.decode!(encoded) == [ nil, nil, "t", diff --git a/test/phoenix/test/conn_test.exs b/test/phoenix/test/conn_test.exs index 724de7a040..3fe58f6bb7 100644 --- a/test/phoenix/test/conn_test.exs +++ b/test/phoenix/test/conn_test.exs @@ -328,13 +328,13 @@ defmodule Phoenix.Test.ConnTest do build_conn(:get, "/") |> resp(200, "ok") |> json_response(200) end - assert_raise Jason.DecodeError, - "unexpected byte at position 0: 0x6F (\"o\")", fn -> + assert_raise JSON.DecodeError, + "invalid byte 111 at position (byte offset) 0", fn -> build_conn(:get, "/") |> put_resp_content_type("application/json") |> resp(200, "ok") |> json_response(200) end - assert_raise Jason.DecodeError, ~r/unexpected end of input at position 0/, fn -> + assert_raise JSON.DecodeError, ~r/unexpected end of JSON binary at position \(byte offset\) 0/, fn -> build_conn(:get, "/") |> put_resp_content_type("application/json") |> resp(200, "") From 802a479e26316c002752cb788171fdbb0cbdbcaf Mon Sep 17 00:00:00 2001 From: Egor Mikhnevich Date: Wed, 17 Sep 2025 17:57:42 +0200 Subject: [PATCH 2/6] Update documentation --- guides/controllers.md | 2 +- guides/json_and_apis.md | 2 +- guides/plug.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/guides/controllers.md b/guides/controllers.md index 572f4407af..40ae3abdbe 100644 --- a/guides/controllers.md +++ b/guides/controllers.md @@ -94,7 +94,7 @@ end Now [`/hello/Frank`] in your browser should display `From messenger Frank` as plain text without any HTML. -A step beyond this is rendering pure JSON with the [`json/2`] function. We need to pass it something that the [Jason library](`Jason`) can decode into JSON, such as a map. (Jason is one of Phoenix's dependencies.) +A step beyond this is rendering pure JSON with the [`json/2`] function. We need to pass it something that the [JSON library](https://hexdocs.pm/elixir/JSON.html) can decode into JSON, such as a map. ```elixir def show(conn, %{"messenger" => messenger}) do diff --git a/guides/json_and_apis.md b/guides/json_and_apis.md index e2705c9a3e..7e1683fc6c 100644 --- a/guides/json_and_apis.md +++ b/guides/json_and_apis.md @@ -171,7 +171,7 @@ defmodule HelloWeb.UrlJSON do end ``` -This view is very simple. The `index` function receives all URLs, and converts them into a list of maps. Those maps are placed inside the data key at the root, exactly as we saw when interfacing with our application from `cURL`. In other words, our JSON view converts our complex data into simple Elixir data-structures. Once our view layer returns, Phoenix uses the `Jason` library to encode JSON and send the response to the client. +This view is very simple. The `index` function receives all URLs, and converts them into a list of maps. Those maps are placed inside the data key at the root, exactly as we saw when interfacing with our application from `cURL`. In other words, our JSON view converts our complex data into simple Elixir data-structures. Once our view layer returns, Phoenix uses the built-in [JSON library](https://hexdocs.pm/elixir/1.18/JSON.html) to encode JSON and send the response to the client. If you explore the remaining controller, you will learn the `show` action is similar to the `index` one. For `create`, `update`, and `delete` actions, Phoenix uses one other important feature, called "Action fallback". diff --git a/guides/plug.md b/guides/plug.md index 308a60b877..36c2766f2e 100644 --- a/guides/plug.md +++ b/guides/plug.md @@ -153,7 +153,7 @@ The default endpoint plugs do quite a lot of work. Here they are in order: - `Plug.Telemetry` - adds instrumentation points so Phoenix can log the request path, status code and request time by default. -- `Plug.Parsers` - parses the request body when a known parser is available. By default, this plug can handle URL-encoded, multipart and JSON content (with `Jason`). The request body is left untouched if the request content-type cannot be parsed. +- `Plug.Parsers` - parses the request body when a known parser is available. By default, this plug can handle URL-encoded, multipart and JSON content. The request body is left untouched if the request content-type cannot be parsed. - `Plug.MethodOverride` - converts the request method to PUT, PATCH or DELETE for POST requests with a valid `_method` parameter. From 006941dcf88239ffc13dcab0ec96f8813499650b Mon Sep 17 00:00:00 2001 From: Egor Mikhnevich Date: Tue, 23 Sep 2025 21:03:29 +0200 Subject: [PATCH 3/6] Keep Jason as the default runtime library --- lib/phoenix.ex | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/phoenix.ex b/lib/phoenix.ex index bcc5226508..0d484dc898 100644 --- a/lib/phoenix.ex +++ b/lib/phoenix.ex @@ -6,8 +6,6 @@ defmodule Phoenix do """ use Application - @default_json_library if Code.ensure_loaded?(JSON), do: JSON, else: Jason - @doc false def start(_type, _args) do # Warm up caches @@ -47,7 +45,8 @@ defmodule Phoenix do """ def json_library do - Application.get_env(:phoenix, :json_library, @default_json_library) + # TODO: Change the default json library to JSON in Phoenix v2.0 + Application.get_env(:phoenix, :json_library, Jason) end @doc """ From 41d3c0d8107050f86716883aa4bcd8b2ce9586a5 Mon Sep 17 00:00:00 2001 From: Egor Mikhnevich Date: Tue, 23 Sep 2025 21:25:47 +0200 Subject: [PATCH 4/6] Determine the dev json library at runtime --- config/config.exs | 3 ++- integration_test/config/config.exs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config/config.exs b/config/config.exs index 69f5ee1176..77a4e046c6 100644 --- a/config/config.exs +++ b/config/config.exs @@ -5,7 +5,8 @@ config :logger, :console, format: "\n$time $metadata[$level] $message\n" config :phoenix, - json_library: JSON, + # TODO: Remove the `json_library` check once `JSON` becomes the standard `Phoenix.json_library/1` + json_library: (if Code.ensure_loaded?(JSON), do: JSON, else: Jason), stacktrace_depth: 20, trim_on_html_eex_engine: false diff --git a/integration_test/config/config.exs b/integration_test/config/config.exs index 541288954f..308398d6cc 100644 --- a/integration_test/config/config.exs +++ b/integration_test/config/config.exs @@ -1,6 +1,7 @@ import Config -config :phoenix, :json_library, JSON +# TODO: Remove the `json_library` check once `JSON` becomes the standard `Phoenix.json_library/1` +config :phoenix, :json_library, (if Code.ensure_loaded?(JSON), do: JSON, else: Jason) config :swoosh, api_client: false From 16e96f6041acc7d3e84a458e536588c8b649feb0 Mon Sep 17 00:00:00 2001 From: Egor Mikhnevich Date: Tue, 23 Sep 2025 21:26:32 +0200 Subject: [PATCH 5/6] Adopt tests to support both JSON and Jason libraries --- test/phoenix/socket/v1_json_serializer_test.exs | 6 +++--- test/phoenix/socket/v2_json_serializer_test.exs | 2 +- test/phoenix/test/conn_test.exs | 12 +++++++++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/test/phoenix/socket/v1_json_serializer_test.exs b/test/phoenix/socket/v1_json_serializer_test.exs index 00943d556d..0bfb7f4881 100644 --- a/test/phoenix/socket/v1_json_serializer_test.exs +++ b/test/phoenix/socket/v1_json_serializer_test.exs @@ -24,7 +24,7 @@ defmodule Phoenix.Socket.V1.JSONSerializerTest do msg = %Message{topic: "t", event: "e", payload: "m"} encoded = encode!(@serializer, msg) - assert JSON.decode!(encoded) == %{ + assert Phoenix.json_library().decode!(encoded) == %{ "event" => "e", "payload" => "m", "ref" => nil, @@ -36,7 +36,7 @@ defmodule Phoenix.Socket.V1.JSONSerializerTest do msg = %Reply{topic: "t", ref: "null"} encoded = encode!(@serializer, msg) - assert JSON.decode!(encoded) == %{ + assert Phoenix.json_library().decode!(encoded) == %{ "event" => "phx_reply", "payload" => %{"response" => nil, "status" => nil}, "ref" => "null", @@ -61,7 +61,7 @@ defmodule Phoenix.Socket.V1.JSONSerializerTest do msg = %Broadcast{topic: "t", event: "e", payload: "m"} encoded = fastlane!(@serializer, msg) - assert JSON.decode!(encoded) == %{ + assert Phoenix.json_library().decode!(encoded) == %{ "event" => "e", "payload" => "m", "ref" => nil, diff --git a/test/phoenix/socket/v2_json_serializer_test.exs b/test/phoenix/socket/v2_json_serializer_test.exs index 25b1d4505b..acfb2ef7e9 100644 --- a/test/phoenix/socket/v2_json_serializer_test.exs +++ b/test/phoenix/socket/v2_json_serializer_test.exs @@ -102,7 +102,7 @@ defmodule Phoenix.Socket.V2.JSONSerializerTest do msg = %Reply{topic: "t", payload: %{m: 1}} encoded = encode!(@serializer, msg) - assert JSON.decode!(encoded) == [ + assert Phoenix.json_library().decode!(encoded) == [ nil, nil, "t", diff --git a/test/phoenix/test/conn_test.exs b/test/phoenix/test/conn_test.exs index 3fe58f6bb7..ffadd98065 100644 --- a/test/phoenix/test/conn_test.exs +++ b/test/phoenix/test/conn_test.exs @@ -328,13 +328,19 @@ defmodule Phoenix.Test.ConnTest do build_conn(:get, "/") |> resp(200, "ok") |> json_response(200) end - assert_raise JSON.DecodeError, - "invalid byte 111 at position (byte offset) 0", fn -> + json_error = + case Phoenix.json_library() do + JSON -> JSON.DecodeError + Jason -> Jason.DecodeError + end + + assert_raise json_error, + ~r/invalid byte 111 at position \(byte offset\) 0|unexpected byte at position 0: 0x6F \("o"\)/, fn -> build_conn(:get, "/") |> put_resp_content_type("application/json") |> resp(200, "ok") |> json_response(200) end - assert_raise JSON.DecodeError, ~r/unexpected end of JSON binary at position \(byte offset\) 0/, fn -> + assert_raise json_error, ~r/unexpected end of JSON binary at position \(byte offset\) 0|unexpected end of input at position 0/, fn -> build_conn(:get, "/") |> put_resp_content_type("application/json") |> resp(200, "") From 9122de9fc201c2077be87e363fff787eaee3de58 Mon Sep 17 00:00:00 2001 From: Egor Mikhnevich Date: Sat, 11 Oct 2025 12:24:00 +0200 Subject: [PATCH 6/6] Ignore json assertion messages --- test/phoenix/test/conn_test.exs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/phoenix/test/conn_test.exs b/test/phoenix/test/conn_test.exs index ffadd98065..57534544a7 100644 --- a/test/phoenix/test/conn_test.exs +++ b/test/phoenix/test/conn_test.exs @@ -334,13 +334,12 @@ defmodule Phoenix.Test.ConnTest do Jason -> Jason.DecodeError end - assert_raise json_error, - ~r/invalid byte 111 at position \(byte offset\) 0|unexpected byte at position 0: 0x6F \("o"\)/, fn -> + assert_raise json_error, fn -> build_conn(:get, "/") |> put_resp_content_type("application/json") |> resp(200, "ok") |> json_response(200) end - assert_raise json_error, ~r/unexpected end of JSON binary at position \(byte offset\) 0|unexpected end of input at position 0/, fn -> + assert_raise json_error, fn -> build_conn(:get, "/") |> put_resp_content_type("application/json") |> resp(200, "")