Skip to content

Conversation

@chrismccord
Copy link
Member

Closes #2693

defp get_json_encoder do
Application.get_env(:phoenix, :format_encoders)
|> Keyword.get(:json, Poison)
|> Keyword.get(:json, Jason)
Copy link
Member

Choose a reason for hiding this comment

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

This is backwards incompatible, I am afraid.

Copy link
Member Author

@chrismccord chrismccord Jan 30, 2018

Choose a reason for hiding this comment

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

Yeah, I was going to ask how we should handle it. We can make it an explicit config, but then we'd still need to ship with the poison* dep, which is not ideal

Copy link
Member

Choose a reason for hiding this comment

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

We cannot simply change to Jason as that is backwards incompatible, since Jason is not fully compatible with Poison.

In my opinion, we should expose this function behind Phoenix.json_encoder (it can be @doc false) and use it throughout the codebase but we should still default to poison. In new apps, we generate:

config :phoenix, :format_encoders, json: Jason

Copy link
Member

Choose a reason for hiding this comment

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

Another reason to this clean up is that Ecto can only migrate to Jason by default in 3.0. So I am afraid that making this easily achievable is the best way to go for now.

alias Phoenix.Template

@encoders [html: Phoenix.Template.HTML, json: Poison, js: Phoenix.Template.HTML]
@encoders [html: Phoenix.Template.HTML, json: Jason, js: Phoenix.Template.HTML]
Copy link
Member

Choose a reason for hiding this comment

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

Also backwards incompatible.

Copy link
Member

@josevalim josevalim Feb 1, 2018

Choose a reason for hiding this comment

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

FWIW, this is still backwards incompatible. :) It should be Phoenix.json.

%Message{topic: msg.topic, event: msg.event, payload: msg.payload}
|> to_list()
|> Poison.encode!()
|> Jason.encode!()
Copy link
Member

Choose a reason for hiding this comment

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

We should start using encode_to_iodata here.

"""
def encode!(msg) do
{:socket_push, :text, msg |> to_list() |> Poison.encode!()}
{:socket_push, :text, msg |> to_list() |> Jason.encode!()}
Copy link
Member

Choose a reason for hiding this comment

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

Encode to iodata.

Assuming we had a route for `get "/our_path/:id"` mapped to this `show` action, going to `/our_path/15` in your browser should display `Showing id 15` 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 [Poison library](https://github.com/devinus/poison) can parse into JSON, such as a map. (Poison 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 [Jason library](https://github.com/michalmuskala/jason) can parse into JSON, such as a map. (Jason is one of Phoenix's dependencies.)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be "encode" instead of "parse"?

%Message{topic: msg.topic, event: msg.event, payload: msg.payload}
|> to_list()
|> Poison.encode!()
|> Jason.encode!()
Copy link
Contributor

Choose a reason for hiding this comment

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

We might use encode_to_iodata! here as well, like in the websocket serializer.

@v1_msg_json ["{\"", [[], "event"], "\":", [34, [], "e", 34], ",\"", [[], "payload"], "\":", [34, [], "m", 34], ",\"", [[], "ref"], "\":", "null", ",\"", [[], "topic"], "\":", [34, [], "t", 34], 125]
@v1_reply_json ["{\"", [[], "event"], "\":", [34, [], "phx_reply", 34], ",\"", [[], "payload"], "\":", ["{\"", [[], "response"], "\":", "null", ",\"", [[], "status"], "\":", "null", 125], ",\"", [[], "ref"], "\":", [34, [], "null", 34], ",\"", [[], "topic"], "\":", [34, [], "t", 34], 125]
@v1_fastlane_json ["{\"", [[], "event"], "\":", [34, [], "e", 34], ",\"", [[], "payload"], "\":", [34, [], "m", 34], ",\"", [[], "ref"], "\":", "null", ",\"", [[], "topic"], "\":", [34, [], "t", 34], 125]
@v2_msg_json [91, "null", 44, "null", 44, [34, [], "t", 34], 44, [34, [], "e", 34], 44, [34, [], "m", 34], 93]
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to flatten the iodata before assertion in the test? It should be less brittle in case of changes in the library.

Copy link
Member

Choose a reason for hiding this comment

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

👍

Copy link
Member

Choose a reason for hiding this comment

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

Ping!

@chrismccord
Copy link
Member Author

Recap:

  • Require explicit configuration of json format_encoder for Phoenix to boot. (may also default to Poison)
  • Use explicit config for new applications that configures Jason in user's mix config.
  • Add :jason to new application's deps.
  • Add :jason as optional dep in phoenix (tests)
  • Drop Poison from Phoenix deps

@chrismccord
Copy link
Member Author

@josevalim updated as discussed. Can you take another look? Thanks!

lib/phoenix.ex Outdated

children = [
# Ensure format encoders are loaded before starting
worker(Task, [fn -> ensure_format_encoders() end], restart: :transient),
Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not thrilled about this approach but it's the only way to get reasonable error messages out of start. The downside is the default max_restarts shows the runtime error three times. Better ideas?

Copy link
Contributor

Choose a reason for hiding this comment

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

Would making it a temporary worker solve this?

Copy link
Member

Choose a reason for hiding this comment

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

Starting a worker sounds like too much.

@chrismccord if you are not happy with the error message when raising directly inside application, you have two options:

  1. Log and return {:error, :missing_format_encoder}
  2. Log and let the app boot anyway

metadata: [:request_id]

# Use Jason for JSON parsing in Phoenix
config :phoenix, :format_encoders, json: Jason
Copy link
Member

Choose a reason for hiding this comment

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

We also need to generate the configuration for Ecto:

config :ecto, :json_library, Jason

Copy link
Member Author

Choose a reason for hiding this comment

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

Is this deprecated? Should we generate config :#{db_adapter}, :json_library, Jason instead?

@chrismccord
Copy link
Member Author

@josevalim can you take a final pass? I generated the "deprecated" for 3.0 Ecto config, but we can update to the ecto adapter config once 3.0 ships.

config :phoenix, :format_encoders, json: Jason
<%= if ecto do %>
config :ecto, :json_library, Jason
<% end %>
Copy link
Member

Choose a reason for hiding this comment

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

OCD check: how is this going to be rendered in practice? Maybe it would be better to be defined as:

config :phoenix, :format_encoders, json: Jason<%= if ecto do %>
config :ecto, :json_library, Jason
<% end %>

?

use Mix.Config

<%= if ecto do %>config :<%= app_name %>, ecto_repos: [<%= app_module %>.Repo]<% end %>
<%= if ecto do %>config :ecto, :json_library, Jason<% end %>
Copy link
Member

Choose a reason for hiding this comment

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

We can probably merge the two conditionals into one.

Application.ensure_all_started(:cowboy)

# Clear format encoder cache from startup and inject Jason
Application.delete_env(:phoenix, :compiled_format_encoders)
Copy link
Member

Choose a reason for hiding this comment

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

Should we also cache Phoenix.json? Also, should we make it public?

Alternatively we can read Phoenix.json at compile time.

Copy link
Member Author

Choose a reason for hiding this comment

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

We can't read Phoenix.json at compile time because the application needs to be started to fetch application env, but isn't started yet ¯_(ツ)_/¯

Copy link
Member

Choose a reason for hiding this comment

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

No, we don't need to start the application to fetch the environment, only to fetch the defaults in mix.exs. But if the default is in the module, like in this case here, it should be dine.

Any configuration from the user application will also be available.

In any case, we can separate this discussion in two:

  1. Shall we make this public?
  2. Shall we cache it? At runtime or compile time?

Copy link
Member Author

Choose a reason for hiding this comment

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

Recap:

Introduce public Phoenix.json_library/0 with config :phoenix, :json_library configuration. No need to cache as runtime lookup is simple ets call.

@josevalim
Copy link
Member

@chrismccord I have added some comments. Regarding Ecto 3.0, we have no estimate for its release, so I wouldn't worry about it.

Should we make Phoenix.json public? One advantage of doing so is that we don't need to hardcode Jason in the user application as well. For example, in Plug.Parsers we would pass Phoenix.json. One less place to change at the end of the day. Thoughts?

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.

4 participants