From e14db0d2c1edf24479e4ab573fb8e99d196e95fe Mon Sep 17 00:00:00 2001 From: Matt Polzin Date: Mon, 18 Sep 2023 23:33:54 -0500 Subject: [PATCH 1/4] make setting determine if auto links should be added. allow custom links to show up regardless. --- README.md | 3 + lib/jsonapi/serializer.ex | 70 ++++++++++++++------ test/jsonapi/serializer_test.exs | 110 +++++++++++++++++++++++-------- 3 files changed, 136 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 2fb34e4e..ec6ab867 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,7 @@ config :jsonapi, field_transformation: :underscore, remove_links: false, serialize_nil_relationships: false, + add_auto_links: false, json_library: Jason, paginator: nil ``` @@ -224,6 +225,8 @@ config :jsonapi, are `nil` will be omitted during serialization. Setting this to `true` will serialize these relationships, provided they are loaded on the resource. Defaults to `false`. +- **add_auto_links**. `links` data can optionally be auto-added by + setting the configuration above to `true`. Defaults to `true`. - **json_library**. Defaults to [Jason](https://hex.pm/packages/jason). - **paginator**. Module implementing pagination links generation. Defaults to `nil`. diff --git a/lib/jsonapi/serializer.ex b/lib/jsonapi/serializer.ex index d76be9ae..af5f5e7e 100644 --- a/lib/jsonapi/serializer.ex +++ b/lib/jsonapi/serializer.ex @@ -42,7 +42,7 @@ defmodule JSONAPI.Serializer do encoded_data end - merge_links(encoded_data, data, view, conn, query_page, remove_links?(), options) + merge_links(encoded_data, data, view, conn, query_page, add_auto_links?(), options, true) end def encode_data(_view, nil, _conn, _query_includes, _options), do: {[], nil} @@ -67,7 +67,7 @@ defmodule JSONAPI.Serializer do relationships: %{} } - doc = merge_links(encoded_data, data, view, conn, nil, remove_links?(), options) + doc = merge_links(encoded_data, data, view, conn, nil, add_auto_links?(), options, false) doc = case view.meta(data, conn) do @@ -210,42 +210,70 @@ defmodule JSONAPI.Serializer do data: encode_rel_data(rel_view, rel_data) } - merge_related_links(data, info, remove_links?()) + merge_related_links(data, info, add_auto_links?()) end - defp merge_base_links(%{links: links} = doc, data, view, conn) do - view_links = data |> view.links(conn) |> Map.merge(links) - Map.merge(doc, %{links: view_links}) + defp merge_links(doc, data, view, conn, _page, false, _options, is_root_of_doc) do + merge_view_links(doc, data, view, conn, is_root_of_doc) end - defp merge_links(doc, data, view, conn, page, false, options) when is_list(data) do - links = + defp merge_links(doc, data, view, conn, page, true, options, is_root_of_doc) do + doc + |> merge_auto_links(data, view, conn, page, options) + |> merge_view_links(data, view, conn, is_root_of_doc) + end + + defp merge_view_links(doc, data, view, conn, is_root_of_doc) + + defp merge_view_links(%{links: links} = doc, data, view, conn, false) do + # intentionally take the view.links and merge them into the existing links, + # allowing the custom defined links for the view to override any auto-links + # generated by this library: + view_links = Map.merge(links, view.links(data, conn)) + + new_doc = Map.merge(doc, %{links: view_links}) + + case new_doc do + %{links: links} when links == %{} -> + Map.delete(doc, :links) + + doc -> + doc + end + end + + defp merge_view_links(doc, data, view, conn, false), + do: merge_view_links(Map.merge(doc, %{links: %{}}), data, view, conn, false) + + defp merge_view_links(doc, _data, _view, _conn, true), do: doc + + defp merge_auto_links(doc, data, view, conn, page, options) + when is_list(data) do + auto_links = data |> view.pagination_links(conn, page, options) - |> Map.merge(%{self: view.url_for_pagination(data, conn, page)}) + |> Map.merge(%{ + self: view.url_for_pagination(data, conn, page) + }) - doc - |> Map.merge(%{links: links}) - |> merge_base_links(data, view, conn) + Map.merge(doc, %{links: auto_links}) end - defp merge_links(doc, data, view, conn, _page, false, _options) do - doc - |> Map.merge(%{links: %{self: view.url_for(data, conn)}}) - |> merge_base_links(data, view, conn) - end + defp merge_auto_links(doc, data, view, conn, _page, _options) do + auto_links = %{self: view.url_for(data, conn)} - defp merge_links(doc, _data, _view, _conn, _page, _remove_links, _options), do: doc + Map.merge(doc, %{links: auto_links}) + end defp merge_related_links( encoded_data, {rel_view, rel_data, rel_url, conn}, - false = _remove_links + true = _add_auto_links ) do Map.merge(encoded_data, %{links: %{self: rel_url, related: rel_view.url_for(rel_data, conn)}}) end - defp merge_related_links(encoded_rel_data, _info, _remove_links), do: encoded_rel_data + defp merge_related_links(encoded_rel_data, _info, _add_auto_links), do: encoded_rel_data @spec encode_rel_data(module(), map() | list()) :: map() | nil def encode_rel_data(_view, nil), do: nil @@ -302,7 +330,7 @@ defmodule JSONAPI.Serializer do |> List.flatten() end - defp remove_links?, do: Application.get_env(:jsonapi, :remove_links, false) + defp add_auto_links?, do: Application.get_env(:jsonapi, :add_auto_links, true) @spec serialize_nil_relationships? :: boolean() defp serialize_nil_relationships?, do: Application.get_env(:jsonapi, :serialize_nil_relationships, false) diff --git a/test/jsonapi/serializer_test.exs b/test/jsonapi/serializer_test.exs index 262bc0d2..119a04e4 100644 --- a/test/jsonapi/serializer_test.exs +++ b/test/jsonapi/serializer_test.exs @@ -19,6 +19,27 @@ defmodule JSONAPI.SerializerTest do end end + defmodule PostViewWithLinks do + use JSONAPI.View + + def fields, do: [:text, :body, :full_description, :inserted_at] + def meta(data, _conn), do: %{meta_text: "meta_#{data[:text]}"} + def type, do: "mytype" + + def relationships do + [ + author: {JSONAPI.SerializerTest.UserView, :include}, + best_comments: {JSONAPI.SerializerTest.CommentView, :include} + ] + end + + def links(_data, _conn) do + %{ + self: "https://website.com/api/posts/1" + } + end + end + defmodule PageBasedPaginator do @moduledoc """ Page based pagination strategy @@ -838,33 +859,66 @@ defmodule JSONAPI.SerializerTest do assert included == [] end - test "serialize does not include links if remove_links is configured" do - data = %{ - id: 1, - text: "Hello", - body: "Hello world", - full_description: "This_is_my_description", - author: %{id: 2, username: "jbonds", first_name: "jerry", last_name: "bonds"}, - best_comments: [ - %{ - id: 5, - text: "greatest comment ever", - user: %{id: 4, username: "jack", last_name: "bronds"} - } - ] - } + describe "when configured to not add auto links" do + setup do + Application.put_env(:jsonapi, :add_auto_links, false) - Application.put_env(:jsonapi, :remove_links, true) + on_exit(fn -> + Application.delete_env(:jsonapi, :add_auto_links) + end) - encoded = Serializer.serialize(PostView, data, nil) + {:ok, []} + end - relationships = encoded[:data][:relationships] + test "serialize does not include auto links" do + data = %{ + id: 1, + text: "Hello", + body: "Hello world", + full_description: "This_is_my_description", + author: %{id: 2, username: "jbonds", first_name: "jerry", last_name: "bonds"}, + best_comments: [ + %{ + id: 5, + text: "greatest comment ever", + user: %{id: 4, username: "jack", last_name: "bronds"} + } + ] + } + + encoded = Serializer.serialize(PostView, data, nil) + + relationships = encoded[:data][:relationships] + + refute relationships[:links] + refute encoded[:data][:links] + refute encoded[:links] + end + + test "serialize still adds custom links" do + data = %{ + id: 1, + text: "Hello", + body: "Hello world", + full_description: "This_is_my_description", + author: %{id: 2, username: "jbonds", first_name: "jerry", last_name: "bonds"}, + best_comments: [ + %{ + id: 5, + text: "greatest comment ever", + user: %{id: 4, username: "jack", last_name: "bronds"} + } + ] + } - refute relationships[:links] - refute encoded[:data][:links] - refute encoded[:links] + encoded = Serializer.serialize(PostViewWithLinks, data, nil) - Application.delete_env(:jsonapi, :remove_links) + relationships = encoded[:data][:relationships] + + refute relationships[:links] + assert encoded[:data][:links][:self] == "https://website.com/api/posts/1" + refute encoded[:links] + end end test "serialize includes pagination links if page-based pagination is requested" do @@ -905,9 +959,13 @@ defmodule JSONAPI.SerializerTest do test "serialize can include arbitrary, user-defined, links" do data = %{id: 1} - assert %{ - links: links - } = Serializer.serialize(ExpensiveResourceView, data, nil) + assert %{data: resp, links: links} = Serializer.serialize(ExpensiveResourceView, data, nil) + + assert %{links: resource_links} = resp + + assert links == %{ + self: "/expensive-resource/#{data.id}" + } expected_links = %{ self: "/expensive-resource/#{data.id}", @@ -920,7 +978,7 @@ defmodule JSONAPI.SerializerTest do } } - assert expected_links == links + assert expected_links == resource_links end test "serialize returns a null data if it receives a null data" do From 527b7157dcfbcf726a6a05501bbe353dc5043646 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 24 Mar 2026 16:51:18 -0500 Subject: [PATCH 2/4] only add links by default if not configured to remove them --- lib/jsonapi/serializer.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/jsonapi/serializer.ex b/lib/jsonapi/serializer.ex index af5f5e7e..a84f7168 100644 --- a/lib/jsonapi/serializer.ex +++ b/lib/jsonapi/serializer.ex @@ -330,7 +330,11 @@ defmodule JSONAPI.Serializer do |> List.flatten() end - defp add_auto_links?, do: Application.get_env(:jsonapi, :add_auto_links, true) + defp add_auto_links? do + remove_links? = Application.get_env(:jsonapi, :remove_links, false) + + Application.get_env(:jsonapi, :add_auto_links, !remove_links?) + end @spec serialize_nil_relationships? :: boolean() defp serialize_nil_relationships?, do: Application.get_env(:jsonapi, :serialize_nil_relationships, false) From c204f3d9bebc3eb75ca7a56a62023fe02850a6d4 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 24 Mar 2026 16:51:46 -0500 Subject: [PATCH 3/4] update documentation to be more verbose around how adding and removing links relate to each other --- README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ec6ab867..01c6716d 100644 --- a/README.md +++ b/README.md @@ -198,8 +198,8 @@ config :jsonapi, scheme: "https", namespace: "/api", field_transformation: :underscore, - remove_links: false, serialize_nil_relationships: false, + remove_links: false, add_auto_links: false, json_library: Jason, paginator: nil @@ -219,14 +219,20 @@ config :jsonapi, `:dasherize`. If your API uses underscores (e.g. `"favorite_color": "red"`) set to `:underscore`. To transform only the top-level field keys, use `:camelize_shallow` or `:dasherize_shallow`. -- **remove_links**. `links` data can optionally be removed from the payload via - setting the configuration above to `true`. Defaults to `false`. - **serialize_nil_relationships**. By default, relationships on a resource that are `nil` will be omitted during serialization. Setting this to `true` will serialize these relationships, provided they are loaded on the resource. Defaults to `false`. +- **remove_links**. `links` data can optionally be removed from the payload via + setting the configuration above to `true`. Defaults to `false`. This setting + removes all links, not just auto-added links. - **add_auto_links**. `links` data can optionally be auto-added by - setting the configuration above to `true`. Defaults to `true`. + setting the configuration above to `true`. Defaults to `true`. This setting is + overruled by `remove_links` for backwards compatibility -- setting both to + `true` results in adding links and then removing them. Only setting + `add_auto_links` to `true` (and leaving `remove_links` `false` as is its + default) will serialize your explicitly created links but not add any + automatically generated links on top. - **json_library**. Defaults to [Jason](https://hex.pm/packages/jason). - **paginator**. Module implementing pagination links generation. Defaults to `nil`. From 1e72c7e7e3f4ff1906aa73774a4170c2e5d90869 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Tue, 24 Mar 2026 17:08:11 -0500 Subject: [PATCH 4/4] re-introduce backwards compatibiltiy that was removed initially --- lib/jsonapi/serializer.ex | 18 +++++++--- test/jsonapi/serializer_test.exs | 58 ++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/lib/jsonapi/serializer.ex b/lib/jsonapi/serializer.ex index a84f7168..69a7dbff 100644 --- a/lib/jsonapi/serializer.ex +++ b/lib/jsonapi/serializer.ex @@ -228,8 +228,14 @@ defmodule JSONAPI.Serializer do defp merge_view_links(%{links: links} = doc, data, view, conn, false) do # intentionally take the view.links and merge them into the existing links, # allowing the custom defined links for the view to override any auto-links - # generated by this library: - view_links = Map.merge(links, view.links(data, conn)) + # generated by this library UNLESS configured to remove links (backwards + # compatibility): + view_links = + if remove_links?() do + %{} + else + Map.merge(links, view.links(data, conn)) + end new_doc = Map.merge(doc, %{links: view_links}) @@ -330,10 +336,12 @@ defmodule JSONAPI.Serializer do |> List.flatten() end - defp add_auto_links? do - remove_links? = Application.get_env(:jsonapi, :remove_links, false) + defp remove_links? do + Application.get_env(:jsonapi, :remove_links, false) + end - Application.get_env(:jsonapi, :add_auto_links, !remove_links?) + defp add_auto_links? do + Application.get_env(:jsonapi, :add_auto_links, !remove_links?()) end @spec serialize_nil_relationships? :: boolean() diff --git a/test/jsonapi/serializer_test.exs b/test/jsonapi/serializer_test.exs index 119a04e4..a7fbc35c 100644 --- a/test/jsonapi/serializer_test.exs +++ b/test/jsonapi/serializer_test.exs @@ -859,6 +859,35 @@ defmodule JSONAPI.SerializerTest do assert included == [] end + test "serialize does not include links if remove_links is configured" do + data = %{ + id: 1, + text: "Hello", + body: "Hello world", + full_description: "This_is_my_description", + author: %{id: 2, username: "jbonds", first_name: "jerry", last_name: "bonds"}, + best_comments: [ + %{ + id: 5, + text: "greatest comment ever", + user: %{id: 4, username: "jack", last_name: "bronds"} + } + ] + } + + Application.put_env(:jsonapi, :remove_links, true) + + encoded = Serializer.serialize(PostView, data, nil) + + relationships = encoded[:data][:relationships] + + refute relationships[:links] + refute encoded[:data][:links] + refute encoded[:links] + + Application.delete_env(:jsonapi, :remove_links) + end + describe "when configured to not add auto links" do setup do Application.put_env(:jsonapi, :add_auto_links, false) @@ -919,6 +948,35 @@ defmodule JSONAPI.SerializerTest do assert encoded[:data][:links][:self] == "https://website.com/api/posts/1" refute encoded[:links] end + + test "serialize honors older remove_links config and removes all links" do + data = %{ + id: 1, + text: "Hello", + body: "Hello world", + full_description: "This_is_my_description", + author: %{id: 2, username: "jbonds", first_name: "jerry", last_name: "bonds"}, + best_comments: [ + %{ + id: 5, + text: "greatest comment ever", + user: %{id: 4, username: "jack", last_name: "bronds"} + } + ] + } + + Application.put_env(:jsonapi, :remove_links, true) + + encoded = Serializer.serialize(PostViewWithLinks, data, nil) + + relationships = encoded[:data][:relationships] + + refute relationships[:links] + refute encoded[:data][:links] + refute encoded[:links] + + Application.delete_env(:jsonapi, :remove_links) + end end test "serialize includes pagination links if page-based pagination is requested" do