JsonView - Render json response for Phoenix easily

Hi there, I have just released first version of my library name JsonView that help to render JSON to client.

You can use it with Phoenix.View or use it independently. It helps to manipulate data, and handle rendering association automatically.

Let’s take a look.

First define view modules

  defmodule MyApp.UserView do
      use JsonView
      def render("user.json", %{user: user}) do
      	render_json(user, [:first_name, :last_name, :vatar], [], [])
      end
  end
      
  defmodule MyApp.PostView do
      use JsonView

      # define which fields return without modifying
      @fields [:title, :content, :excerpt, :cover]
      # define which fields that need to format or calculate, you have to define `render_field/2` below
      @custom_fields [:like_count]
      # define which view used to render relationship
      @relationships [author: MyApp.UserView]

      def render("post.json", %{post: post}) do
          # 1st way if `use JsonView`
          render_json(post, @fields, @custom_fields, @relationships)
      end

      def render_field(:like_count, item) do
          # load like_count from some where
      end
  end

And then use it

post = %Post{
	title: "Hello JsonView",
	excerpt: "Now you can render Json easier",
	content: "Install and put it to work",
	cover: nil,
	inserted_at: ~N[2021-07-05 00:00:00],
	updated_at: ~N[2021-07-09 00:00:00],
	author: %User{
		first_name: "Daniel",
		last_name: "James",
		email: "daniel@example.com",
		avatar: nil,
		inserted_at: ~N[2021-06-30 00:00:00]
		updated_at: ~N[2021-07-02 00:00:00]
	}
}

MyApp.PostView.render("post.json", %{post: post})

# or invoke from PostController
render(conn, "post.json", post: post)

This is the result that you can use to return from PhoenixController

%{
	title: "Hello JsonView",
	excerpt: "Now you can render Json easier",
	content: "Install and put it to work",
	cover: nil,
  like_count: nil,
	author: %{
		first_name: "Daniel",
		last_name: "James"
	}
}

If you have any feedback, please comment or create an issue.
Thank you.

I’m not sure I understand the value proposition of this library.
Could you share what was the use case that led to its creation? It seems that a common use case is to treat mapping inside shared/ common views for each schema of the application, which is fairly easy.

Thanks for your question.

When I worked on my project I want to define json schema for my API response. I want it to be able to render some custom data and handle association that defined in Ecto schema.

At first I try ja_serializer. It’s cool but not fit for my need. It supports JSON API spec and at the client side I cannot handle the response naturally. For example

{
  "data": [{
    "type": "articles",
    "id": "1",
    "attributes": {
      "title": "JSON:API paints my bikeshed!",
      "content": "hello world"
    },
    "relationships": {
      "author": {
        "data": { "type": "people", "id": "9" }
      },
  }]
}

I have to do much . to access article title and content.
So I build this module to use in my project and now I extract it to a library.

Aside from how strange it is that you find that too much (which is an opinion, okay) then I still don’t understand why can’t your issue be solved with the get_in function?

I use javascript for client side and I have to do post.attributes.title to get the title instead of just post.title. Or in case of association, to get the author I have to do post.relationships.author.attributes.name. I would like something that same with my Ecto schema.

Well, you can just have MyApp.Post.get_name function that returns post.relationships.author.attributes.name. :man_shrugging:

I mean OK, it’s useful for you, I am not bashing at all, just wondering what’s the value-add over a few simple wrapping functions. The declarative syntax, maybe?

I’m not sure I understand the use case well enough to give an opinion but have you investigated using plain views to do this kind of work before? It seems to me that it would be easier to use views to do those mappings.

For example:

PostView

render("show.json", %{post: post}) do
  post
  |> Map.update(:author, render_one(post.author, UserView, "show.json"))
  |> Map.update(:comments, render_many(post.comments, CommentView, "show.json"))
end

UserView

render("show.json", %{user: user}) do
  # map/ transform user (author)
end

CommentView

render("show.json", %{comment: comment}) do
  # map/ transform comment
end

Actually I use mapping to handle nil and AssocNotLoaded cases and write less code.
I agree that implicit is not good but I’m quite lazy

Thanks for you opinion. I access json api response from Javascript and Flutter. I have checked some libraries and I think they are not suited my needs.