How to use Jason.Encode

Its not entirely clear from the documentation how to use Jason.Encode.

I have the following structure:

  %GoogleApi.Storage.V1.Model.Bucket{
    acl: nil,
    billing: nil,
    cors: nil,
    defaultEventBasedHold: false,
    defaultObjectAcl: nil,
    encryption: nil,
    etag: "CAE=",
    iamConfiguration: %GoogleApi.Storage.V1.Model.BucketIamConfiguration{
      bucketPolicyOnly: %GoogleApi.Storage.V1.Model.BucketIamConfigurationBucketPolicyOnly{
        enabled: false,
        lockedTime: nil
      },
      uniformBucketLevelAccess: %GoogleApi.Storage.V1.Model.BucketIamConfigurationUniformBucketLevelAccess{
        enabled: false,
        lockedTime: nil
      }
    },
    id: "fafdafdaf",
    kind: "storage#bucket",
    labels: nil,
    lifecycle: nil,
    location: "US",
    locationType: "multi-region",
    logging: nil,
    metageneration: "1",
    name: "fafdafdaf",
    owner: nil,
    projectNumber: "454545",
    retentionPolicy: nil,
    selfLink: "https://www.googleapis.com/storage/v1/b/fafdafdaf",
    storageClass: "STANDARD",
    timeCreated: ~U[2020-03-05 02:00:31.923Z],
    updated: ~U[2020-03-05 02:00:31.923Z],
    versioning: nil,
    website: nil
  },
  %GoogleApi.Storage.V1.Model.Bucket{
    acl: nil,
    billing: nil,
    cors: nil,
    defaultEventBasedHold: false,
    defaultObjectAcl: nil,
    encryption: nil,
    etag: "CAE=",
    iamConfiguration: %GoogleApi.Storage.V1.Model.BucketIamConfiguration{
      bucketPolicyOnly: %GoogleApi.Storage.V1.Model.BucketIamConfigurationBucketPolicyOnly{
        enabled: false,
        lockedTime: nil
      },
      uniformBucketLevelAccess: %GoogleApi.Storage.V1.Model.BucketIamConfigurationUniformBucketLevelAccess{
        enabled: false,
        lockedTime: nil
      }
    },
    id: "gcs_bucket_api",
    kind: "storage#bucket",
    labels: nil,
    lifecycle: nil,
    location: "US",
    locationType: "multi-region",
    logging: nil,
    metageneration: "1",
    name: "gcs_bucket_api",
    owner: nil,
    projectNumber: "454545",
    retentionPolicy: nil,
    selfLink: "https://www.googleapis.com/storage/v1/b/gcs_bucket_api",
    storageClass: "STANDARD",
    timeCreated: ~U[2020-03-05 01:34:16.147Z],
    updated: ~U[2020-03-05 01:34:16.147Z],
    versioning: nil,
    website: nil
  }
]

I would like to give the whole thing as JSON but it says I need to implement it using @derive Jason.Encode. But I don’t understand how that would be done given the example from documentation adding the following did not help:

"Jason.Encoder protocol must always be explicitly implemented.\n\nIf you own the struct, you can derive the implementation specifying which fields should be encoded to JSON:\n\n @derive {Jason.Encoder, only: [....]}\n defstruct ...\n\nIt is also possible to encode all fields, although this should be used carefully to avoid accidentally leaking private information when new fields are added:\n\n @derive Jason.Encoder\n defstruct ...\n\nFinally, if you don't own the struct you want to encode to JSON, you may use Protocol.derive/3 placed outside of any module:\n\n Protocol.derive(Jason.Encoder, NameOfTheStruct, only: [...])\n

Given the following:

{:ok, token} = Goth.Token.for_scope("https://www.googleapis.com/auth/cloud-platform")
conc = GoogleApi.Storage.V1.Connection.new(token.token)
{:ok, response} = GoogleApi.Storage.V1.Api.Buckets.storage_buckets_list(conc, "PROJECT")

What is the right way to make response.items which is a list of structs with nested structs into JSON using Jason?

Solution I came up without Jason works great, but would be cleaner if I used Jason to encode my structs?

data = Enum.map(response.items, fn x -> 
        x = Map.update!(x, :iamConfiguration, &(Map.from_struct(&1)) )
        x_iamConf = Map.get(x, :iamConfiguration)
    
        x = Map.update!(x, :iamConfiguration, fn x_iamConf -> 
            x_iamConf = Map.update!(x_iamConf, :bucketPolicyOnly, &(Map.from_struct(&1))) 
            x_iamConf = Map.update!(x_iamConf, :uniformBucketLevelAccess, &(Map.from_struct(&1))) 
        end)
    
        x = Map.from_struct(x)
    end)
    json(conn, data)

put @derive {Jason.Encoder} in each module just before the struct is defined. You can use the :only and :except options to define which items in your struct are encoded, e.g.

defmodule A do
  @derive {Jason.Encode, except[:foo]}
  defstruct [:foo, :bar, :baz]
end

will result in bar & baz being encoded

3 Likes

Can you give a more concrete example based on the data structure I provided?

I’m not defining that struct, it’s provided by the google storage api.

I also tried using the Protocol method it mentions in the docs but it gave the same not implemented error.

Man, I’m still stuck on using this library.

defmodule GcsBucket do
  use GcsBucketApiWeb, :controller

  alias Jason.{EncodeError, Encoder}

  def index(conn, _params) do
    {:ok, token} = Goth.Token.for_scope("https://www.googleapis.com/auth/cloud-platform")
    conc = GoogleApi.Storage.V1.Connection.new(token.token)
    {:ok, response} = GoogleApi.Storage.V1.Api.Buckets.storage_buckets_list(conc, "test")
    #{_, data2} = Jason.encode(%{acl: "A", billing: "B", id: "C"})
    {_, data2} = Jason.encode(response.items)
    text(conn, data2)
  end

  defmodule GoogleApi.Storage.V1.Model.Bucket do
    @derive {Jason.Encoder, only: [:acl, :billing, :id]}
    defstruct [:acl, :billing, :id]
  end

  defimpl Jason.Encoder, for: [GoogleApi.Storage.V1.Model.Bucket] do
    def encode(list, opts) do
      %{
        acl: "FOO",
        billing: "BAR", 
        id: "BAZ"
       }
    end
  end
end

It keeps crashing before it

1 Like

Found the solution. I miss Poision. Don’t understand why it works this way though…I’ll have to read about the Protocol library I guess. Thanks for the help though.

defmodule GcsBucketApiWeb.BucketController do
  use GcsBucketApiWeb, :controller
  require Protocol
  Protocol.derive(Jason.Encoder, GoogleApi.Storage.V1.Model.Bucket)
  Protocol.derive(Jason.Encoder, GoogleApi.Storage.V1.Model.BucketIamConfiguration)
  Protocol.derive(Jason.Encoder, GoogleApi.Storage.V1.Model.BucketIamConfigurationBucketPolicyOnly)
  Protocol.derive(Jason.Encoder, GoogleApi.Storage.V1.Model.BucketIamConfigurationUniformBucketLevelAccess)

  alias Jason.{EncodeError, Encoder}


  def index(conn, _params) do
    {:ok, token} = Goth.Token.for_scope("https://www.googleapis.com/auth/cloud-platform")
    conc = GoogleApi.Storage.V1.Connection.new(token.token)
    {:ok, response} = GoogleApi.Storage.V1.Api.Buckets.storage_buckets_list(conc, "test")

    {_, data} = Jason.encode(response.items)
    text(conn, data)
  end
end
1 Like

I was just writing up a small test case - yes, the answer is in the Jason doco guide just before the license. Here’s the minimal test case to represent your problem:

# Modules we don't have code for
defmodule A do
  defstruct [:foo, :bar, :baz]
end

defmodule B do
  defstruct [:a, :y, :z]
end

# We write this code outside of a module definition
require Protocol

Protocol.derive(Jason.Encoder, A)
Protocol.derive(Jason.Encoder, B)

defmodule Blah do
  def test do
    a = %A{foo: "foo", bar: "bar", baz: 2}
    b = %B{a: a, y: "Why?"}

    Jason.encode!(b)

    # Gives use the expected result:
    "{\"a\":{\"bar\":\"bar\",\"baz\":2,\"foo\":\"foo\"},\"y\":\"Why?\",\"z\":null}"
  end
end
1 Like

Sometimes it is simpler to just use Map.from_struct/1 before passing data to Jason :slight_smile:

1 Like