Efficient & idiomatic way of rendering Json inside HTML template in Phoenix

Hello,

I’m trying to render a Json with microformat data in the root of the template.

Initially I did it writing the json in a template, but it seems inneficient, and buggy:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "LocalBusiness",
  <%= if @data.name != nil do %>"name": "<%= @data.name %>"<% end %>,
  <%= if @data.description != nil do %>"description": "<%= @data.description %>"<% end %>,
  <%= if @data.phone != nil do %>"telephone": "<%= @data.phone %>"<% end %>,
  <%= if @data.has_geo_data do %>
  "geo": {
    "@type": "GeoCoordinates",
    "latitude": <%= @data.latitude %>,
    "longitude": <%= @data.longitude %>
  },
  <%= render("geo.json", latitude: @data.latitude, longitude: @data.longitude)%>
  <% end %>
  "address": {
    "@type": "PostalAddress",
    <%= if @data.address.street_address != nil do %>"streetAddress": "<%= @data.address.street_address %>",<% end %>
    <%= if @data.address.address_locality != nil do %>"addressLocality": "<%= @data.address.address_locality %>",<% end %>
    <%= if @data.address.address_region != nil do %>"addressRegion": "<%= @data.address.address_region %>",<% end %>
    <%= if @data.address.postal_code != nil do %>"postalCode": "<%= @data.address.postal_code %>",<% end %>
    <%= if @data.address.address_country_code != nil do %>"addressCountry": "<%= @data.address.address_country_code %>",<% end %>
  }
}
</script>

Too many conditionals, and tje json is not valid because of the last comma.

I thought that maybe I could render a map instead, and did a test only with the geo data:

<%= render("geo.json", latitude: @data.latitude, longitude: @data.longitude) %>

The idea is to render the whole json the same way, but I just started with this section.

This is the view:

  def render("geo.json", %{longitude: longitude, latitude: latitude})
      when is_float(longitude) and is_float(latitude) do
    %{
      "geo" => %{
        "@type" => "GeoCoordinates",
        "latitude" => latitude,
        "longitude" => longitude
      }
    }
    |> Jason.encode_to_iodata!()
    |> raw
  end

  def render("geo.json", _) do
    raw(nil)
  end

(1) First question: Is this efficient because of the usage of Jason.encode_to_iodata() ?or maybe the latest rawmake it inefficient?
(2) Second question: Since I want it to be rendered inside the template, I have to mark it safe using raw but this is data comming from the database that is being introduced by the users. This is not safe.
For latitude & longitude is not that bad, but if the users set the name to <script>..some script code..</script> then we have a security risk.
My initial idea was to iterate witn Enum.map over the map and apply html_escape to each value.
It should be a recursive function because the map can have another map as a value.
Do you think there is a better way of doing this with Phoenix?

Thanks!

Adrián

For such repetitive tasks I would try to simplify with an iteration… something like.

for {k, v} <- @data, k in ~w(name description phone)a, do: ...

I would also consider having some small functions to process each case. (simple, geo, address), not a big one.

And I would consider building a map, then later transform to json. Not building it like if it was html text :slight_smile:

Anyway, it’s not that different when You apply filters to ecto, which is usually solved like this.

Enum.reduce(data, result, fn 
  {:name, val}, result -> ... # do something with result, val and key
  {:description, val}, result -> ...
  {:phone, val}, result -> ...
  _, result -> 
    Logger.debug(fn -> "oops, i did it again" end) 
    result
end)

And after this, You could Enum.join(…, ", "), avoiding the last comma problem, or encode this map to json.

2 Likes

Thanks @kokolegorille !

I think this could be improved by making your data a struct and using the Jason.Encoder protocol.