Repository: phoenixframework/phoenix (elixir)

Issue:
mix tasks phx.gen.live/html generate broken heex templates when given map type attributes
Suggestion for a small improvement, skip over map data type attributes from jsonb columns that currently cause the mix tasks `phx.gen.live` and `phx.gen.html` to generate broken heex templates. For `phx.gen.live`, visiting the show and index pages results in the error "protocol Phoenix.HTML.Safe not implemented for %{...} of type Map." And here's a related [old stackoverflow question](https://stackoverflow.com/questions/35549618/mix-phoenix-gen-html-with-map-field-type) on this. 
current: `<%= for {k, _} <- schema.attrs do %> `

proposed: `<%= for {k, v} when not is_map(v) <- schema.attrs do %>` see: [draft PR](https://github.com/phoenixframework/phoenix/pull/5234/files)


Pull request description:
Skip map inputs in generated forms
Previously when using the html and live generators, generating a map field would result in a text input field. There is no trivial way to input map data in a text field. The map input is now skipped when generating the html and live forms.

This introduces a new issue where the field is missing, but marked as required on the schema. To fix this, a new `optionals` value is available on the generator schema, maps are automatically included into this schema. Any optionals are now skipped when generating `validate_required` in the schema file.

This closes #5235 

Patch:
diff --git a/installer/lib/phx_new/generator.ex b/installer/lib/phx_new/generator.ex
index 71a9b9fa1..1a4e80761 100644
--- a/installer/lib/phx_new/generator.ex
+++ b/installer/lib/phx_new/generator.ex
@@ -115,12 +115,12 @@ defmodule Phx.New.Generator do
         {:error, _} -> "import Config\n"
       end
 
-    with :error <- split_with_self(contents, "use Mix.Config"),
-         :error <- split_with_self(contents, "import Config") do
-      Mix.raise(~s[Could not find "use Mix.Config" or "import Config" in #{inspect(file)}])
-    else
-      [left, middle, right] ->
-        write_formatted!(file, [left, middle, ?\n, ?\n, to_inject, right])
+    case :binary.split(contents, "import Config") do
+      [left, right] ->
+        write_formatted!(file, [left, to_inject, right])
+
+      [_] ->
+        Mix.raise(~s[Could not find "import Config" in #{inspect(file)}])
     end
   end
 
@@ -141,11 +141,11 @@ defmodule Phx.New.Generator do
           """
       end
 
-    case split_with_self(contents, "if config_env() == :prod do") do
-      [left, middle, right] ->
-        write_formatted!(file, [left, middle, ?\n, to_inject, right])
+    case :binary.split(contents, "if config_env() == :prod do") do
+      [left, right] ->
+        write_formatted!(file, [left, "if config_env() == :prod do\n", to_inject, right])
 
-      :error ->
+      [_] ->
         Mix.raise(~s[Could not find "if config_env() == :prod do" in #{inspect(file)}])
     end
   end
@@ -167,13 +167,6 @@ defmodule Phx.New.Generator do
     end
   end
 
-  defp split_with_self(contents, text) do
-    case :binary.split(contents, text) do
-      [left, right] -> [left, text, right]
-      [_] -> :error
-    end
-  end
-
   def in_umbrella?(app_path) do
     umbrella = Path.expand(Path.join([app_path, "..", ".."]))
     mix_path = Path.join(umbrella, "mix.exs")
@@ -268,11 +261,15 @@ defmodule Phx.New.Generator do
     adapter_config = binding[:adapter_config]
 
     config_inject(project_path, "config/dev.exs", """
+    import Config
+
     # Configure your database
     config :#{binding[:app_name]}, #{binding[:app_module]}.Repo#{kw_to_config(adapter_config[:dev])}
     """)
 
     config_inject(project_path, "config/test.exs", """
+    import Config
+
     # Configure your database
     #
     # The MIX_TEST_PARTITION environment variable can be used
diff --git a/installer/templates/phx_assets/tailwind.config.js b/installer/templates/phx_assets/tailwind.config.js
index d1d8837e6..c352bc38b 100644
--- a/installer/templates/phx_assets/tailwind.config.js
+++ b/installer/templates/phx_assets/tailwind.config.js
@@ -9,7 +9,7 @@ module.exports = {
   content: [
     "./js/**/*.js",
     "../lib/<%= @lib_web_name || @app_name %>.ex",
-    "../lib/<%= @lib_web_name || @app_name %>/**/*.ex"
+    "../lib/<%= @lib_web_name || @app_name %>/**/*.*ex"
   ],
   theme: {
     extend: {
diff --git a/installer/templates/phx_single/config/config.exs b/installer/templates/phx_single/config/config.exs
index c80f690f2..7e7b56703 100644
--- a/installer/templates/phx_single/config/config.exs
+++ b/installer/templates/phx_single/config/config.exs
@@ -5,8 +5,9 @@
 # is restricted to this project.
 
 # General application configuration
-import Config<%= if @namespaced? || @ecto || @generators do %>
+import Config
 
+<%= if @namespaced? || @ecto || @generators do %>
 config :<%= @app_name %><%= if @namespaced? do %>,
   namespace: <%= @app_module %><% end %><%= if @ecto do %>,
   ecto_repos: [<%= @app_module %>.Repo]<% end %><%= if @generators do %>,
diff --git a/installer/templates/phx_single/config/prod.exs b/installer/templates/phx_single/config/prod.exs
index d0a599fc8..144b0c818 100644
--- a/installer/templates/phx_single/config/prod.exs
+++ b/installer/templates/phx_single/config/prod.exs
@@ -1,5 +1,6 @@
-import Config<%= if @javascript or @css do %>
+import Config
 
+<%= if @javascript or @css do %>
 # Note we also include the path to a cache manifest
 # containing the digested version of static files. This
 # manifest is generated by the `mix assets.deploy` task,
diff --git a/installer/templates/phx_umbrella/apps/app_name/config/config.exs b/installer/templates/phx_umbrella/apps/app_name/config/config.exs
index 95535f41c..973789cd6 100644
--- a/installer/templates/phx_umbrella/apps/app_name/config/config.exs
+++ b/installer/templates/phx_umbrella/apps/app_name/config/config.exs
@@ -1,4 +1,7 @@
-<%= if @namespaced? || @ecto do %># Configure Mix tasks and generators
+import Config
+
+<%= if @namespaced? || @ecto do %>
+# Configure Mix tasks and generators
 config :<%= @app_name %><%= if @namespaced? do %>,
   namespace: <%= @app_module %><% end %><%= if @ecto do %>,
   ecto_repos: [<%= @app_module %>.Repo]<% end %><% end %><%= if @mailer do %>
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/config.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/config.exs
index fd46d3641..3af233f39 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/config.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/config.exs
@@ -1,3 +1,5 @@
+import Config
+
 <%= if @namespaced? || @ecto || @generators do %>
 config :<%= @web_app_name %><%= if @namespaced? do %>,
   namespace: <%= @web_namespace %><% end %><%= if @ecto do %>,
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs
index 64a5e9723..66466e13e 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs
@@ -1,3 +1,5 @@
+import Config
+
 # For development, we disable any cache and enable
 # debugging and code reloading.
 #
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs
index 4acae1147..315068e97 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs
@@ -1,3 +1,5 @@
+import Config
+
 # Note we also include the path to a cache manifest
 # containing the digested version of static files. This
 # manifest is generated by the `mix phx.digest` task,
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/runtime.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/runtime.exs
index c57731ee7..8b85007bd 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/runtime.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/runtime.exs
@@ -1,3 +1,5 @@
+import Config
+
 # The secret key base is used to sign/encrypt cookies and other secrets.
 # A default value is used in config/dev.exs and config/test.exs but you
 # want to use a different value for prod and you most likely don't want
diff --git a/installer/templates/phx_umbrella/config/extra_config.exs b/installer/templates/phx_umbrella/config/extra_config.exs
index 07cba2b0a..68d1c6605 100644
--- a/installer/templates/phx_umbrella/config/extra_config.exs
+++ b/installer/templates/phx_umbrella/config/extra_config.exs
@@ -1,3 +1,5 @@
+import Config
+
 # Configures Elixir's Logger
 config :logger, :console,
   format: "$time $metadata[$level] $message\n",
diff --git a/lib/mix/phoenix/schema.ex b/lib/mix/phoenix/schema.ex
index 670322864..8e9c11c9a 100644
--- a/lib/mix/phoenix/schema.ex
+++ b/lib/mix/phoenix/schema.ex
@@ -28,6 +28,7 @@ defmodule Mix.Phoenix.Schema do
             migration_defaults: nil,
             migration?: false,
             params: %{},
+            optionals: [],
             sample_id: nil,
             web_path: nil,
             web_namespace: nil,
@@ -86,7 +87,6 @@ defmodule Mix.Phoenix.Schema do
     api_prefix = Application.get_env(otp_app, :generators)[:api_prefix] || "/api"
     embedded? = Keyword.get(opts, :embedded, false)
     generate? = Keyword.get(opts, :schema, true)
-
     singular =
       module
       |> Module.split()
@@ -97,6 +97,8 @@ defmodule Mix.Phoenix.Schema do
     string_attr = string_attr(types)
     create_params = params(attrs, :create)
 
+    optionals = for {key, :map} <- types, do: key, into: []
+
     default_params_key =
       case Enum.at(create_params, 0) do
         {key, _} -> key
@@ -118,6 +120,7 @@ defmodule Mix.Phoenix.Schema do
       plural: schema_plural,
       singular: singular,
       collection: collection,
+      optionals: optionals,
       assocs: assocs,
       types: types,
       defaults: schema_defaults(attrs),
@@ -250,6 +253,14 @@ defmodule Mix.Phoenix.Schema do
     end)
   end
 
+  @doc """
+  Return the required fields in the schema. Anything not in the `optionals` list
+  is considered required.
+  """
+  def required_fields(schema) do
+    Enum.reject(schema.attrs, fn {key, _} -> key in schema.optionals end)
+  end
+
   def type_and_opts_for_schema({:enum, opts}),
     do: ~s|Ecto.Enum, values: #{inspect(Keyword.get(opts, :values))}|
 
diff --git a/lib/mix/tasks/phx.gen.html.ex b/lib/mix/tasks/phx.gen.html.ex
index 01262692b..3243dc463 100644
--- a/lib/mix/tasks/phx.gen.html.ex
+++ b/lib/mix/tasks/phx.gen.html.ex
@@ -172,7 +172,9 @@ defmodule Mix.Tasks.Phx.Gen.Html do
 
   @doc false
   def inputs(%Schema{} = schema) do
-    Enum.map(schema.attrs, fn
+    schema.attrs
+    |> Enum.reject(fn {_key, type} -> type == :map end)
+    |> Enum.map(fn
       {key, :integer} ->
         ~s(<.input field={f[#{inspect(key)}]} type="number" label="#{label(key)}" />)
 
@@ -246,6 +248,9 @@ defmodule Mix.Tasks.Phx.Gen.Html do
       lines = input |> String.split("\n") |> Enum.reject(&(&1 == ""))
 
       case lines do
+        [] ->
+          []
+
         [line] ->
           [columns, line]
 
diff --git a/lib/mix/tasks/phx.gen.live.ex b/lib/mix/tasks/phx.gen.live.ex
index ea9a29135..825b27190 100644
--- a/lib/mix/tasks/phx.gen.live.ex
+++ b/lib/mix/tasks/phx.gen.live.ex
@@ -271,7 +271,9 @@ defmodule Mix.Tasks.Phx.Gen.Live do
 
   @doc false
   def inputs(%Schema{} = schema) do
-    Enum.map(schema.attrs, fn
+    schema.attrs
+    |> Enum.reject(fn {_key, type} -> type == :map end)
+    |> Enum.map(fn
       {_, {:references, _}} ->
         nil
 
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index 84afdbc8f..d6253067b 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -261,8 +261,8 @@ defmodule Phoenix.Endpoint do
 
   """
 
-  @type topic :: String.t
-  @type event :: String.t
+  @type topic :: String.t()
+  @type event :: String.t()
   @type msg :: map | {:binary, binary}
 
   require Logger
@@ -275,7 +275,7 @@ defmodule Phoenix.Endpoint do
   Starts endpoint's configuration cache and possibly the servers for
   handling requests.
   """
-  @callback start_link(keyword) :: Supervisor.on_start
+  @callback start_link(keyword) :: Supervisor.on_start()
 
   @doc """
   Access the endpoint configuration given by key.
@@ -293,54 +293,54 @@ defmodule Phoenix.Endpoint do
   Invoked when the endpoint supervisor starts, allows dynamically
   configuring the endpoint from system environment or other runtime sources.
   """
-  @callback init(:supervisor, config :: Keyword.t) :: {:ok, Keyword.t}
+  @callback init(:supervisor, config :: Keyword.t()) :: {:ok, Keyword.t()}
 
   # Paths and URLs
 
   @doc """
   Generates the endpoint base URL, but as a `URI` struct.
   """
-  @callback struct_url() :: URI.t
+  @callback struct_url() :: URI.t()
 
   @doc """
   Generates the endpoint base URL without any path information.
   """
-  @callback url() :: String.t
+  @callback url() :: String.t()
 
   @doc """
   Generates the path information when routing to this endpoint.
   """
-  @callback path(path :: String.t) :: String.t
+  @callback path(path :: String.t()) :: String.t()
 
   @doc """
   Generates the static URL without any path information.
   """
-  @callback static_url() :: String.t
+  @callback static_url() :: String.t()
 
   @doc """
   Generates a route to a static file in `priv/static`.
   """
-  @callback static_path(path :: String.t) :: String.t
+  @callback static_path(path :: String.t()) :: String.t()
 
   @doc """
   Generates an integrity hash to a static file in `priv/static`.
   """
-  @callback static_integrity(path :: String.t) :: String.t | nil
+  @callback static_integrity(path :: String.t()) :: String.t() | nil
 
   @doc """
   Generates a two item tuple containing the `static_path` and `static_integrity`.
   """
-  @callback static_lookup(path :: String.t) :: {String.t, String.t} | {String.t, nil}
+  @callback static_lookup(path :: String.t()) :: {String.t(), String.t()} | {String.t(), nil}
 
   @doc """
   Returns the script name from the :url configuration.
   """
-  @callback script_name() :: [String.t]
+  @callback script_name() :: [String.t()]
 
   @doc """
   Returns the host from the :url configuration.
   """
-  @callback host() :: String.t
+  @callback host() :: String.t()
 
   # Channels
 
@@ -349,7 +349,7 @@ defmodule Phoenix.Endpoint do
 
   See `Phoenix.PubSub.subscribe/3` for options.
   """
-  @callback subscribe(topic, opts :: Keyword.t) :: :ok | {:error, term}
+  @callback subscribe(topic, opts :: Keyword.t()) :: :ok | {:error, term}
 
   @doc """
   Unsubscribes the caller from the given topic.
@@ -404,7 +404,7 @@ defmodule Phoenix.Endpoint do
 
   defp config(opts) do
     quote do
-      @otp_app unquote(opts)[:otp_app] || raise "endpoint expects :otp_app to be given"
+      @otp_app unquote(opts)[:otp_app] || raise("endpoint expects :otp_app to be given")
       var!(config) = Phoenix.Endpoint.Supervisor.config(@otp_app, __MODULE__)
       var!(code_reloading?) = var!(config)[:code_reloader]
 
@@ -478,7 +478,8 @@ defmodule Phoenix.Endpoint do
           banner: {Phoenix.Endpoint.RenderErrors, :__debugger_banner__, []},
           style: [
             primary: "#EB532D",
-            logo: "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNzEgNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgoJPHBhdGggZD0ibTI2LjM3MSAzMy40NzctLjU1Mi0uMWMtMy45Mi0uNzI5LTYuMzk3LTMuMS03LjU3LTYuODI5LS43MzMtMi4zMjQuNTk3LTQuMDM1IDMuMDM1LTQuMTQ4IDEuOTk1LS4wOTIgMy4zNjIgMS4wNTUgNC41NyAyLjM5IDEuNTU3IDEuNzIgMi45ODQgMy41NTggNC41MTQgNS4zMDUgMi4yMDIgMi41MTUgNC43OTcgNC4xMzQgOC4zNDcgMy42MzQgMy4xODMtLjQ0OCA1Ljk1OC0xLjcyNSA4LjM3MS0zLjgyOC4zNjMtLjMxNi43NjEtLjU5MiAxLjE0NC0uODg2bC0uMjQxLS4yODRjLTIuMDI3LjYzLTQuMDkzLjg0MS02LjIwNS43MzUtMy4xOTUtLjE2LTYuMjQtLjgyOC04Ljk2NC0yLjU4Mi0yLjQ4Ni0xLjYwMS00LjMxOS0zLjc0Ni01LjE5LTYuNjExLS43MDQtMi4zMTUuNzM2LTMuOTM0IDMuMTM1LTMuNi45NDguMTMzIDEuNzQ2LjU2IDIuNDYzIDEuMTY1LjU4My40OTMgMS4xNDMgMS4wMTUgMS43MzggMS40OTMgMi44IDIuMjUgNi43MTIgMi4zNzUgMTAuMjY1LS4wNjgtNS44NDItLjAyNi05LjgxNy0zLjI0LTEzLjMwOC03LjMxMy0xLjM2Ni0xLjU5NC0yLjctMy4yMTYtNC4wOTUtNC43ODUtMi42OTgtMy4wMzYtNS42OTItNS43MS05Ljc5LTYuNjIzQzEyLjgtLjYyMyA3Ljc0NS4xNCAyLjg5MyAyLjM2MSAxLjkyNiAyLjgwNC45OTcgMy4zMTkgMCA0LjE0OWMuNDk0IDAgLjc2My4wMDYgMS4wMzIgMCAyLjQ0Ni0uMDY0IDQuMjggMS4wMjMgNS42MDIgMy4wMjQuOTYyIDEuNDU3IDEuNDE1IDMuMTA0IDEuNzYxIDQuNzk4LjUxMyAyLjUxNS4yNDcgNS4wNzguNTQ0IDcuNjA1Ljc2MSA2LjQ5NCA0LjA4IDExLjAyNiAxMC4yNiAxMy4zNDYgMi4yNjcuODUyIDQuNTkxIDEuMTM1IDcuMTcyLjU1NVpNMTAuNzUxIDMuODUyYy0uOTc2LjI0Ni0xLjc1Ni0uMTQ4LTIuNTYtLjk2MiAxLjM3Ny0uMzQzIDIuNTkyLS40NzYgMy44OTctLjUyOC0uMTA3Ljg0OC0uNjA3IDEuMzA2LTEuMzM2IDEuNDlabTMyLjAwMiAzNy45MjRjLS4wODUtLjYyNi0uNjItLjkwMS0xLjA0LTEuMjI4LTEuODU3LTEuNDQ2LTQuMDMtMS45NTgtNi4zMzMtMi0xLjM3NS0uMDI2LTIuNzM1LS4xMjgtNC4wMzEtLjYxLS41OTUtLjIyLTEuMjYtLjUwNS0xLjI0NC0xLjI3Mi4wMTUtLjc4LjY5My0xIDEuMzEtMS4xODQuNTA1LS4xNSAxLjAyNi0uMjQ3IDEuNi0uMzgyLTEuNDYtLjkzNi0yLjg4Ni0xLjA2NS00Ljc4Ny0uMy0yLjk5MyAxLjIwMi01Ljk0MyAxLjA2LTguOTI2LS4wMTctMS42ODQtLjYwOC0zLjE3OS0xLjU2My00LjczNS0yLjQwOGwtLjA0My4wM2EyLjk2IDIuOTYgMCAwIDAgLjA0LS4wMjljLS4wMzgtLjExNy0uMTA3LS4xMi0uMTk3LS4wNTRsLjEyMi4xMDdjMS4yOSAyLjExNSAzLjAzNCAzLjgxNyA1LjAwNCA1LjI3MSAzLjc5MyAyLjggNy45MzYgNC40NzEgMTIuNzg0IDMuNzNBNjYuNzE0IDY2LjcxNCAwIDAgMSAzNyA0MC44NzdjMS45OC0uMTYgMy44NjYuMzk4IDUuNzUzLjg5OVptLTkuMTQtMzAuMzQ1Yy0uMTA1LS4wNzYtLjIwNi0uMjY2LS40Mi0uMDY5IDEuNzQ1IDIuMzYgMy45ODUgNC4wOTggNi42ODMgNS4xOTMgNC4zNTQgMS43NjcgOC43NzMgMi4wNyAxMy4yOTMuNTEgMy41MS0xLjIxIDYuMDMzLS4wMjggNy4zNDMgMy4zOC4xOS0zLjk1NS0yLjEzNy02LjgzNy01Ljg0My03LjQwMS0yLjA4NC0uMzE4LTQuMDEuMzczLTUuOTYyLjk0LTUuNDM0IDEuNTc1LTEwLjQ4NS43OTgtMTUuMDk0LTIuNTUzWm0yNy4wODUgMTUuNDI1Yy43MDguMDU5IDEuNDE2LjEyMyAyLjEyNC4xODUtMS42LTEuNDA1LTMuNTUtMS41MTctNS41MjMtMS40MDQtMy4wMDMuMTctNS4xNjcgMS45MDMtNy4xNCAzLjk3Mi0xLjczOSAxLjgyNC0zLjMxIDMuODctNS45MDMgNC42MDQuMDQzLjA3OC4wNTQuMTE3LjA2Ni4xMTcuMzUuMDA1LjY5OS4wMjEgMS4wNDcuMDA1IDMuNzY4LS4xNyA3LjMxNy0uOTY1IDEwLjE0LTMuNy44OS0uODYgMS42ODUtMS44MTcgMi41NDQtMi43MS43MTYtLjc0NiAxLjU4NC0xLjE1OSAyLjY0NS0xLjA3Wm0tOC43NTMtNC42N2MtMi44MTIuMjQ2LTUuMjU0IDEuNDA5LTcuNTQ4IDIuOTQzLTEuNzY2IDEuMTgtMy42NTQgMS43MzgtNS43NzYgMS4zNy0uMzc0LS4wNjYtLjc1LS4xMTQtMS4xMjQtLjE3bC0uMDEzLjE1NmMuMTM1LjA3LjI2NS4xNTEuNDA1LjIwNy4zNTQuMTQuNzAyLjMwOCAxLjA3LjM5NSA0LjA4My45NzEgNy45OTIuNDc0IDExLjUxNi0xLjgwMyAyLjIyMS0xLjQzNSA0LjUyMS0xLjcwNyA3LjAxMy0xLjMzNi4yNTIuMDM4LjUwMy4wODMuNzU2LjEwNy4yMzQuMDIyLjQ3OS4yNTUuNzk1LjAwMy0yLjE3OS0xLjU3NC00LjUyNi0yLjA5Ni03LjA5NC0xLjg3MlptLTEwLjA0OS05LjU0NGMxLjQ3NS4wNTEgMi45NDMtLjE0MiA0LjQ4Ni0xLjA1OS0uNDUyLjA0LS42NDMuMDQtLjgyNy4wNzYtMi4xMjYuNDI0LTQuMDMzLS4wNC01LjczMy0xLjM4My0uNjIzLS40OTMtMS4yNTctLjk3NC0xLjg4OS0xLjQ1Ny0yLjUwMy0xLjkxNC01LjM3NC0yLjU1NS04LjUxNC0yLjUuMDUuMTU0LjA1NC4yNi4xMDguMzE1IDMuNDE3IDMuNDU1IDcuMzcxIDUuODM2IDEyLjM2OSA2LjAwOFptMjQuNzI3IDE3LjczMWMtMi4xMTQtMi4wOTctNC45NTItMi4zNjctNy41NzgtLjUzNyAxLjczOC4wNzggMy4wNDMuNjMyIDQuMTAxIDEuNzI4LjM3NC4zODguNzYzLjc2OCAxLjE4MiAxLjEwNiAxLjYgMS4yOSA0LjMxMSAxLjM1MiA1Ljg5Ni4xNTUtMS44NjEtLjcyNi0xLjg2MS0uNzI2LTMuNjAxLTIuNDUyWm0tMjEuMDU4IDE2LjA2Yy0xLjg1OC0zLjQ2LTQuOTgxLTQuMjQtOC41OS00LjAwOGE5LjY2NyA5LjY2NyAwIDAgMSAyLjk3NyAxLjM5Yy44NC41ODYgMS41NDcgMS4zMTEgMi4yNDMgMi4wNTUgMS4zOCAxLjQ3MyAzLjUzNCAyLjM3NiA0Ljk2MiAyLjA3LS42NTYtLjQxMi0xLjIzOC0uODQ4LTEuNTkyLTEuNTA3Wm0xNy4yOS0xOS4zMmMwLS4wMjMuMDAxLS4wNDUuMDAzLS4wNjhsLS4wMDYuMDA2LjAwNi0uMDA2LS4wMzYtLjAwNC4wMjEuMDE4LjAxMi4wNTNabS0yMCAxNC43NDRhNy42MSA3LjYxIDAgMCAwLS4wNzItLjA0MS4xMjcuMTI3IDAgMCAwIC4wMTUuMDQzYy4wMDUuMDA4LjAzOCAwIC4wNTgtLjAwMlptLS4wNzItLjA0MS0uMDA4LS4wMzQtLjAwOC4wMS4wMDgtLjAxLS4wMjItLjAwNi4wMDUuMDI2LjAyNC4wMTRaIgogICAgICAgICAgICBmaWxsPSIjRkQ0RjAwIiAvPgo8L3N2Zz4K"
+            logo:
+              "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNzEgNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgoJPHBhdGggZD0ibTI2LjM3MSAzMy40NzctLjU1Mi0uMWMtMy45Mi0uNzI5LTYuMzk3LTMuMS03LjU3LTYuODI5LS43MzMtMi4zMjQuNTk3LTQuMDM1IDMuMDM1LTQuMTQ4IDEuOTk1LS4wOTIgMy4zNjIgMS4wNTUgNC41NyAyLjM5IDEuNTU3IDEuNzIgMi45ODQgMy41NTggNC41MTQgNS4zMDUgMi4yMDIgMi41MTUgNC43OTcgNC4xMzQgOC4zNDcgMy42MzQgMy4xODMtLjQ0OCA1Ljk1OC0xLjcyNSA4LjM3MS0zLjgyOC4zNjMtLjMxNi43NjEtLjU5MiAxLjE0NC0uODg2bC0uMjQxLS4yODRjLTIuMDI3LjYzLTQuMDkzLjg0MS02LjIwNS43MzUtMy4xOTUtLjE2LTYuMjQtLjgyOC04Ljk2NC0yLjU4Mi0yLjQ4Ni0xLjYwMS00LjMxOS0zLjc0Ni01LjE5LTYuNjExLS43MDQtMi4zMTUuNzM2LTMuOTM0IDMuMTM1LTMuNi45NDguMTMzIDEuNzQ2LjU2IDIuNDYzIDEuMTY1LjU4My40OTMgMS4xNDMgMS4wMTUgMS43MzggMS40OTMgMi44IDIuMjUgNi43MTIgMi4zNzUgMTAuMjY1LS4wNjgtNS44NDItLjAyNi05LjgxNy0zLjI0LTEzLjMwOC03LjMxMy0xLjM2Ni0xLjU5NC0yLjctMy4yMTYtNC4wOTUtNC43ODUtMi42OTgtMy4wMzYtNS42OTItNS43MS05Ljc5LTYuNjIzQzEyLjgtLjYyMyA3Ljc0NS4xNCAyLjg5MyAyLjM2MSAxLjkyNiAyLjgwNC45OTcgMy4zMTkgMCA0LjE0OWMuNDk0IDAgLjc2My4wMDYgMS4wMzIgMCAyLjQ0Ni0uMDY0IDQuMjggMS4wMjMgNS42MDIgMy4wMjQuOTYyIDEuNDU3IDEuNDE1IDMuMTA0IDEuNzYxIDQuNzk4LjUxMyAyLjUxNS4yNDcgNS4wNzguNTQ0IDcuNjA1Ljc2MSA2LjQ5NCA0LjA4IDExLjAyNiAxMC4yNiAxMy4zNDYgMi4yNjcuODUyIDQuNTkxIDEuMTM1IDcuMTcyLjU1NVpNMTAuNzUxIDMuODUyYy0uOTc2LjI0Ni0xLjc1Ni0uMTQ4LTIuNTYtLjk2MiAxLjM3Ny0uMzQzIDIuNTkyLS40NzYgMy44OTctLjUyOC0uMTA3Ljg0OC0uNjA3IDEuMzA2LTEuMzM2IDEuNDlabTMyLjAwMiAzNy45MjRjLS4wODUtLjYyNi0uNjItLjkwMS0xLjA0LTEuMjI4LTEuODU3LTEuNDQ2LTQuMDMtMS45NTgtNi4zMzMtMi0xLjM3NS0uMDI2LTIuNzM1LS4xMjgtNC4wMzEtLjYxLS41OTUtLjIyLTEuMjYtLjUwNS0xLjI0NC0xLjI3Mi4wMTUtLjc4LjY5My0xIDEuMzEtMS4xODQuNTA1LS4xNSAxLjAyNi0uMjQ3IDEuNi0uMzgyLTEuNDYtLjkzNi0yLjg4Ni0xLjA2NS00Ljc4Ny0uMy0yLjk5MyAxLjIwMi01Ljk0MyAxLjA2LTguOTI2LS4wMTctMS42ODQtLjYwOC0zLjE3OS0xLjU2My00LjczNS0yLjQwOGwtLjA0My4wM2EyLjk2IDIuOTYgMCAwIDAgLjA0LS4wMjljLS4wMzgtLjExNy0uMTA3LS4xMi0uMTk3LS4wNTRsLjEyMi4xMDdjMS4yOSAyLjExNSAzLjAzNCAzLjgxNyA1LjAwNCA1LjI3MSAzLjc5MyAyLjggNy45MzYgNC40NzEgMTIuNzg0IDMuNzNBNjYuNzE0IDY2LjcxNCAwIDAgMSAzNyA0MC44NzdjMS45OC0uMTYgMy44NjYuMzk4IDUuNzUzLjg5OVptLTkuMTQtMzAuMzQ1Yy0uMTA1LS4wNzYtLjIwNi0uMjY2LS40Mi0uMDY5IDEuNzQ1IDIuMzYgMy45ODUgNC4wOTggNi42ODMgNS4xOTMgNC4zNTQgMS43NjcgOC43NzMgMi4wNyAxMy4yOTMuNTEgMy41MS0xLjIxIDYuMDMzLS4wMjggNy4zNDMgMy4zOC4xOS0zLjk1NS0yLjEzNy02LjgzNy01Ljg0My03LjQwMS0yLjA4NC0uMzE4LTQuMDEuMzczLTUuOTYyLjk0LTUuNDM0IDEuNTc1LTEwLjQ4NS43OTgtMTUuMDk0LTIuNTUzWm0yNy4wODUgMTUuNDI1Yy43MDguMDU5IDEuNDE2LjEyMyAyLjEyNC4xODUtMS42LTEuNDA1LTMuNTUtMS41MTctNS41MjMtMS40MDQtMy4wMDMuMTctNS4xNjcgMS45MDMtNy4xNCAzLjk3Mi0xLjczOSAxLjgyNC0zLjMxIDMuODctNS45MDMgNC42MDQuMDQzLjA3OC4wNTQuMTE3LjA2Ni4xMTcuMzUuMDA1LjY5OS4wMjEgMS4wNDcuMDA1IDMuNzY4LS4xNyA3LjMxNy0uOTY1IDEwLjE0LTMuNy44OS0uODYgMS42ODUtMS44MTcgMi41NDQtMi43MS43MTYtLjc0NiAxLjU4NC0xLjE1OSAyLjY0NS0xLjA3Wm0tOC43NTMtNC42N2MtMi44MTIuMjQ2LTUuMjU0IDEuNDA5LTcuNTQ4IDIuOTQzLTEuNzY2IDEuMTgtMy42NTQgMS43MzgtNS43NzYgMS4zNy0uMzc0LS4wNjYtLjc1LS4xMTQtMS4xMjQtLjE3bC0uMDEzLjE1NmMuMTM1LjA3LjI2NS4xNTEuNDA1LjIwNy4zNTQuMTQuNzAyLjMwOCAxLjA3LjM5NSA0LjA4My45NzEgNy45OTIuNDc0IDExLjUxNi0xLjgwMyAyLjIyMS0xLjQzNSA0LjUyMS0xLjcwNyA3LjAxMy0xLjMzNi4yNTIuMDM4LjUwMy4wODMuNzU2LjEwNy4yMzQuMDIyLjQ3OS4yNTUuNzk1LjAwMy0yLjE3OS0xLjU3NC00LjUyNi0yLjA5Ni03LjA5NC0xLjg3MlptLTEwLjA0OS05LjU0NGMxLjQ3NS4wNTEgMi45NDMtLjE0MiA0LjQ4Ni0xLjA1OS0uNDUyLjA0LS42NDMuMDQtLjgyNy4wNzYtMi4xMjYuNDI0LTQuMDMzLS4wNC01LjczMy0xLjM4My0uNjIzLS40OTMtMS4yNTctLjk3NC0xLjg4OS0xLjQ1Ny0yLjUwMy0xLjkxNC01LjM3NC0yLjU1NS04LjUxNC0yLjUuMDUuMTU0LjA1NC4yNi4xMDguMzE1IDMuNDE3IDMuNDU1IDcuMzcxIDUuODM2IDEyLjM2OSA2LjAwOFptMjQuNzI3IDE3LjczMWMtMi4xMTQtMi4wOTctNC45NTItMi4zNjctNy41NzgtLjUzNyAxLjczOC4wNzggMy4wNDMuNjMyIDQuMTAxIDEuNzI4LjM3NC4zODguNzYzLjc2OCAxLjE4MiAxLjEwNiAxLjYgMS4yOSA0LjMxMSAxLjM1MiA1Ljg5Ni4xNTUtMS44NjEtLjcyNi0xLjg2MS0uNzI2LTMuNjAxLTIuNDUyWm0tMjEuMDU4IDE2LjA2Yy0xLjg1OC0zLjQ2LTQuOTgxLTQuMjQtOC41OS00LjAwOGE5LjY2NyA5LjY2NyAwIDAgMSAyLjk3NyAxLjM5Yy44NC41ODYgMS41NDcgMS4zMTEgMi4yNDMgMi4wNTUgMS4zOCAxLjQ3MyAzLjUzNCAyLjM3NiA0Ljk2MiAyLjA3LS42NTYtLjQxMi0xLjIzOC0uODQ4LTEuNTkyLTEuNTA3Wm0xNy4yOS0xOS4zMmMwLS4wMjMuMDAxLS4wNDUuMDAzLS4wNjhsLS4wMDYuMDA2LjAwNi0uMDA2LS4wMzYtLjAwNC4wMjEuMDE4LjAxMi4wNTNabS0yMCAxNC43NDRhNy42MSA3LjYxIDAgMCAwLS4wNzItLjA0MS4xMjcuMTI3IDAgMCAwIC4wMTUuMDQzYy4wMDUuMDA4LjAzOCAwIC4wNTgtLjAwMlptLS4wNzItLjA0MS0uMDA4LS4wMzQtLjAwOC4wMS4wMDgtLjAxLS4wMjItLjAwNi4wMDUuMDI2LjAyNC4wMTRaIgogICAgICAgICAgICBmaWxsPSIjRkQ0RjAwIiAvPgo8L3N2Zz4K"
           ]
       end
 
@@ -604,8 +605,11 @@ defmodule Phoenix.Endpoint do
       and the second item being the `static_integrity`.
       """
       def static_lookup(path) do
-        Phoenix.Config.cache(__MODULE__, {:__phoenix_static__, path},
-                             &Phoenix.Endpoint.Supervisor.static_lookup(&1, path))
+        Phoenix.Config.cache(
+          __MODULE__,
+          {:__phoenix_static__, path},
+          &Phoenix.Endpoint.Supervisor.static_lookup(&1, path)
+        )
       end
     end
   end
@@ -632,7 +636,7 @@ defmodule Phoenix.Endpoint do
       end
 
     quote do
-      defoverridable [call: 2]
+      defoverridable call: 2
 
       # Inline render errors so we set the endpoint before calling it.
       def call(conn, opts) do
@@ -644,11 +648,25 @@ defmodule Phoenix.Endpoint do
         rescue
           e in Plug.Conn.WrapperError ->
             %{conn: conn, kind: kind, reason: reason, stack: stack} = e
-            Phoenix.Endpoint.RenderErrors.__catch__(conn, kind, reason, stack, config(:render_errors))
+
+            Phoenix.Endpoint.RenderErrors.__catch__(
+              conn,
+              kind,
+              reason,
+              stack,
+              config(:render_errors)
+            )
         catch
           kind, reason ->
             stack = __STACKTRACE__
-            Phoenix.Endpoint.RenderErrors.__catch__(conn, kind, reason, stack, config(:render_errors))
+
+            Phoenix.Endpoint.RenderErrors.__catch__(
+              conn,
+              kind,
+              reason,
+              stack,
+              config(:render_errors)
+            )
         end
       end
 
@@ -698,23 +716,23 @@ defmodule Phoenix.Endpoint do
       |> Enum.join("/")
       |> Plug.Router.Utils.build_path_match()
 
-      conn_ast =
-        if vars == [] do
-          quote do
-            conn
-          end
-        else
-          params =
-            for var <- vars,
-                param = Atom.to_string(var),
-                not match?("_" <> _, param),
-                do: {param, Macro.var(var, nil)}
-
-          quote do
-            params = %{unquote_splicing(params)}
-            %Plug.Conn{conn | path_params: params, params: params}
-          end
+    conn_ast =
+      if vars == [] do
+        quote do
+          conn
         end
+      else
+        params =
+          for var <- vars,
+              param = Atom.to_string(var),
+              not match?("_" <> _, param),
+              do: {param, Macro.var(var, nil)}
+
+        quote do
+          params = %{unquote_splicing(params)}
+          %Plug.Conn{conn | path_params: params, params: params}
+        end
+      end
 
     {conn_ast, path}
   end
@@ -847,9 +865,9 @@ defmodule Phoenix.Endpoint do
 
     * `:trace_context_headers` - a list of all trace context headers. Supported
       headers are defined by the [W3C Trace Context Specification](https://www.w3.org/TR/trace-context-1/).
-      These headers are necessary for libraries such as [OpenTelemetry](https://opentelemetry.io/) to extract
-      trace propagation information to know this request is part of a larger trace
-      in progress.
+      These headers are necessary for libraries such as [OpenTelemetry](https://opentelemetry.io/)
+      to extract trace propagation information to know this request is part of a
+      larger trace in progress.
 
     * `:x_headers` - all request headers that have an "x-" prefix
 
@@ -890,6 +908,22 @@ defmodule Phoenix.Endpoint do
         ]
   ```
 
+  > #### Where are my headers? {: .tip}
+  >
+  > Phoenix only gives you limited access to the connection headers for security
+  > reasons. WebSockets are cross-domain, which means that, when a user "John Doe"
+  > visits a malicious website, the malicious website can open up a WebSocket
+  > connection to your application, and the browser will gladly submit John Doe's
+  > authentication/cookie information. If you were to accept this information as is,
+  > the malicious website would have full control of a WebSocket connection to your
+  > application, authenticated on John Doe's behalf.
+  >
+  > To safe-guard your application, Phoenix limits and validates the connection
+  > information your socket can access. This means your application is safe from
+  > these attacks, but you can't access cookies and other headers in your socket.
+  > You may access the session stored in the connection via the `:connect_info`
+  > option, provided you also pass a csrf token when connecting over WebSocket.
+
   ## Websocket configuration
 
   The following configuration applies only to `:websocket`.
diff --git a/mix.lock b/mix.lock
index a6bbdfd66..599564e81 100644
--- a/mix.lock
+++ b/mix.lock
@@ -6,18 +6,18 @@
   "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
   "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"},
   "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
-  "earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"},
+  "earmark_parser": {:hex, :earmark_parser, "1.4.36", "487ea8ef9bdc659f085e6e654f3c3feea1d36ac3943edf9d2ef6c98de9174c13", [:mix], [], "hexpm", "a524e395634bdcf60a616efe77fd79561bec2e930d8b82745df06ab4e844400a"},
   "ecto": {:hex, :ecto, "3.10.1", "c6757101880e90acc6125b095853176a02da8f1afe056f91f1f90b80c9389822", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d2ac4255f1601bdf7ac74c0ed971102c6829dc158719b94bd30041bbad77f87a"},
   "ecto_sql": {:hex, :ecto_sql, "3.10.1", "6ea6b3036a0b0ca94c2a02613fd9f742614b5cfe494c41af2e6571bb034dd94c", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6a25bdbbd695f12c8171eaff0851fa4c8e72eec1e98c7364402dda9ce11c56b"},
   "esbuild": {:hex, :esbuild, "0.7.0", "ce3afb13cd2c5fd63e13c0e2d0e0831487a97a7696cfa563707342bb825d122a", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "4ae9f4f237c5ebcb001390b8ada65a12fb2bb04f3fe3d1f1692b7a06fbfe8752"},
-  "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"},
+  "ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"},
   "gettext": {:hex, :gettext, "0.20.0", "75ad71de05f2ef56991dbae224d35c68b098dd0e26918def5bb45591d5c8d429", [:mix], [], "hexpm", "1c03b177435e93a47441d7f681a7040bd2a816ece9e2666d1c9001035121eb3d"},
   "hpax": {:hex, :hpax, "0.1.1", "2396c313683ada39e98c20a75a82911592b47e5c24391363343bde74f82396ca", [:mix], [], "hexpm", "0ae7d5a0b04a8a60caf7a39fcf3ec476f35cc2cc16c05abea730d3ce6ac6c826"},
   "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
   "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
   "makeup_eex": {:hex, :makeup_eex, "0.1.1", "89352d5da318d97ae27bbcc87201f274504d2b71ede58ca366af6a5fbed9508d", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.16", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_html, "~> 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d111a0994eaaab09ef1a4b3b313ef806513bb4652152c26c0d7ca2be8402a964"},
   "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
-  "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
+  "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
   "makeup_html": {:hex, :makeup_html, "0.1.0", "b0228fda985e311d8f0d25bed58f8280826633a38d7448cabdd723e116165bcf", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "0ca44e7dcb8d933e010740324470dd8ec947243b51304bd34b8165ef3281edc2"},
   "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
   "mint": {:hex, :mint, "1.4.1", "49b3b6ea35a9a38836d2ad745251b01ca9ec062f7cb66f546bf22e6699137126", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "cd261766e61011a9079cccf8fa9d826e7a397c24fbedf0e11b49312bea629b58"},
diff --git a/priv/templates/phx.gen.schema/schema.ex b/priv/templates/phx.gen.schema/schema.ex
index fc1d4788e..2a8018d7e 100644
--- a/priv/templates/phx.gen.schema/schema.ex
+++ b/priv/templates/phx.gen.schema/schema.ex
@@ -16,7 +16,7 @@ defmodule <%= inspect schema.module %> do
   def changeset(<%= schema.singular %>, attrs) do
     <%= schema.singular %>
     |> cast(attrs, [<%= Enum.map_join(schema.attrs, ", ", &inspect(elem(&1, 0))) %>])
-    |> validate_required([<%= Enum.map_join(schema.attrs, ", ", &inspect(elem(&1, 0))) %>])
+    |> validate_required([<%= Enum.map_join(Mix.Phoenix.Schema.required_fields(schema), ", ", &inspect(elem(&1, 0))) %>])
 <%= for k <- schema.uniques do %>    |> unique_constraint(<%= inspect k %>)
 <% end %>  end
 end


Test patch:
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/test.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/test.exs
index d549f4343..d31c0a629 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/test.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/test.exs
@@ -1,3 +1,5 @@
+import Config
+
 # We don't run a server during test. If one is required,
 # you can enable the server option below.
 config :<%= @web_app_name %>, <%= @endpoint_module %>,
diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs
index d9b96c5e6..617b6a336 100644
--- a/installer/test/phx_new_test.exs
+++ b/installer/test/phx_new_test.exs
@@ -152,7 +152,7 @@ defmodule Mix.Tasks.Phx.NewTest do
       assert_file("phx_blog/assets/css/app.css")
       assert_file "phx_blog/assets/tailwind.config.js", fn file ->
         assert file =~ "phx_blog_web.ex"
-        assert file =~ "phx_blog_web/**/*.ex"
+        assert file =~ "phx_blog_web/**/*.*ex"
       end
 
       assert_file("phx_blog/assets/vendor/heroicons/LICENSE.md")
diff --git a/installer/test/phx_new_umbrella_test.exs b/installer/test/phx_new_umbrella_test.exs
index 5dca16ba0..9fc4258df 100644
--- a/installer/test/phx_new_umbrella_test.exs
+++ b/installer/test/phx_new_umbrella_test.exs
@@ -185,7 +185,7 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
 
       assert_file web_path(@app, "assets/tailwind.config.js"), fn file ->
         assert file =~ "phx_umb_web.ex"
-        assert file =~ "phx_umb_web/**/*.ex"
+        assert file =~ "phx_umb_web/**/*.*ex"
       end
 
       assert_file(web_path(@app, "priv/static/favicon.ico"))
diff --git a/installer/test/phx_new_web_test.exs b/installer/test/phx_new_web_test.exs
index 7b6e7114c..9911ce22c 100644
--- a/installer/test/phx_new_web_test.exs
+++ b/installer/test/phx_new_web_test.exs
@@ -70,7 +70,7 @@ defmodule Mix.Tasks.Phx.New.WebTest do
 
       assert_file "testweb/assets/tailwind.config.js", fn file ->
         assert file =~ "testweb.ex"
-        assert file =~ "testweb/**/*.ex"
+        assert file =~ "testweb/**/*.*ex"
       end
     end
   end
diff --git a/priv/templates/phx.gen.auth/confirmation_live_test.exs b/priv/templates/phx.gen.auth/confirmation_live_test.exs
index 421102141..27c863df5 100644
--- a/priv/templates/phx.gen.auth/confirmation_live_test.exs
+++ b/priv/templates/phx.gen.auth/confirmation_live_test.exs
@@ -54,9 +54,11 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
       assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
                "<%= inspect schema.alias %> confirmation link is invalid or it has expired"
 
+      conn = build_conn()
+
       # when logged in
       {:ok, lv, _html} =
-        build_conn()
+        conn
         |> log_in_<%= schema.singular %>(<%= schema.singular %>)
         |> live(~p"<%= schema.route_prefix %>/confirm/#{token}")
 
diff --git a/test/mix/tasks/phx.gen.html_test.exs b/test/mix/tasks/phx.gen.html_test.exs
index 13723b146..b0eb2d309 100644
--- a/test/mix/tasks/phx.gen.html_test.exs
+++ b/test/mix/tasks/phx.gen.html_test.exs
@@ -54,6 +54,7 @@ defmodule Mix.Tasks.Phx.Gen.HtmlTest do
                       alarm:time
                       alarm_usec:time_usec
                       secret:uuid:redact announcement_date:date alarm:time
+                      metadata:map
                       weight:float user_id:references:users))
 
       assert_file("lib/phoenix/blog/post.ex")
@@ -167,6 +168,7 @@ defmodule Mix.Tasks.Phx.Gen.HtmlTest do
         assert file =~ ~s(<.input field={f[:announcement_date]} type="date")
         assert file =~ ~s(<.input field={f[:alarm]} type="time")
         assert file =~ ~s(<.input field={f[:secret]} type="text" label="Secret" />)
+        refute file =~ ~s(field={f[:metadata]})
 
         assert file =~ """
                  <.input
diff --git a/test/mix/tasks/phx.gen.live_test.exs b/test/mix/tasks/phx.gen.live_test.exs
index bd8e73132..20108f396 100644
--- a/test/mix/tasks/phx.gen.live_test.exs
+++ b/test/mix/tasks/phx.gen.live_test.exs
@@ -65,6 +65,7 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
                       alarm:time
                       alarm_usec:time_usec
                       secret:uuid:redact announcement_date:date alarm:time
+                      metadata:map
                       weight:float user_id:references:users))
 
       assert_file "lib/phoenix/blog/post.ex"
@@ -116,6 +117,7 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
         assert file =~ ~s(<.input field={@form[:announcement_date]} type="date")
         assert file =~ ~s(<.input field={@form[:alarm]} type="time")
         assert file =~ ~s(<.input field={@form[:secret]} type="text" label="Secret" />)
+        refute file =~ ~s(<field={@form[:metadata]})
         assert file =~ """
                 <.input
                   field={@form[:status]}
diff --git a/test/mix/tasks/phx.gen.schema_test.exs b/test/mix/tasks/phx.gen.schema_test.exs
index caab17f49..efb914f5f 100644
--- a/test/mix/tasks/phx.gen.schema_test.exs
+++ b/test/mix/tasks/phx.gen.schema_test.exs
@@ -16,7 +16,7 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
 
   test "build" do
     in_tmp_project "build", fn ->
-      schema = Gen.Schema.build(~w(Blog.Post posts title:string), [])
+      schema = Gen.Schema.build(~w(Blog.Post posts title:string tags:map), [])
 
       assert %Schema{
         alias: Post,
@@ -28,10 +28,11 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
         singular: "post",
         human_plural: "Posts",
         human_singular: "Post",
-        attrs: [title: :string],
-        types: %{title: :string},
+        attrs: [title: :string, tags: :map],
+        types: %{title: :string, tags: :map},
+        optionals: [:tags],
         route_helper: "post",
-        defaults: %{title: ""},
+        defaults: %{title: "", tags: ""},
       } = schema
       assert String.ends_with?(schema.file, "lib/phoenix/blog/post.ex")
     end
@@ -112,6 +113,16 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
     end
   end
 
+  test "does not add maps to the required list", config do
+    in_tmp_project config.test, fn ->
+      Gen.Schema.run(~w(Blog.Post blog_posts title:string tags:map published_at:naive_datetime))
+      assert_file "lib/phoenix/blog/post.ex", fn file ->
+        assert file =~ "cast(attrs, [:title, :tags, :published_at]"
+        assert file =~ "validate_required([:title, :published_at]"
+      end
+    end
+  end
+
   test "generates nested schema", config do
     in_tmp_project config.test, fn ->
       Gen.Schema.run(~w(Blog.Admin.User users name:string))