如何使用 Json 对结构进行自定义编码?

How to have custom enconding for struct using Jason?

背景

我正在尝试使用 Jason 库将结构编码为 json 格式。但是,这没有按预期工作。

代码

假设我有这个结构:

defmodule Test do
   defstruct [:foo, :bar, :baz]
end

并且在使用 Jason.enconde(%Test{foo: 1, bar: 2, baz:3 }) 时,我希望创建此 json:

%{"foo" => 1, "banana" => 5}

错误

据我了解,要实现这一点,我需要在我的结构中实施 Jason.Enconder 协议: https://hexdocs.pm/jason/Jason.Encoder.html

defmodule Test do
   defstruct [:foo, :bar, :baz]
   
   defimpl Jason.Encoder do
      @impl Jason.Encoder 
      def encode(value, opts) do
         Jason.Encode.map(%{foo: Map.get(value, :foo), banana: Map.get(value, :bar, 0) + Map.get(value, :baz, 0)}, opts)
      end
   end
end

但是,这行不通:

Jason.encode(%Test{foo: 1, bar: 2, baz: 3})
{:error,
 %Protocol.UndefinedError{
   description: "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    Protocol.derive(Jason.Encoder, NameOfTheStruct)\n",
   protocol: Jason.Encoder,
   value: %Test{bar: 2, baz: 3, foo: 1}
 }}

据我了解,似乎我只能 select/exclude 键序列化,我不能 transform/add 新键。 由于我拥有相关结构,因此没有必要使用 Protocol.derive

但是我不明白如何利用 Jason.Encoder 协议来实现我想要的。

问题

  1. 我的 objective 是否可以使用 Jason 库,或者这是一个限制?
  2. 我是否错过了对文档的理解并做了一些不正确的事情?

我的猜测是,这是因为在测试文件中编写了协议。协议合并发生在测试文件执行之前,因此协议永远不会成为已编译代码库的一部分。

举例说明...

我在 Phoenix 应用程序中执行了以下操作

  • 进入lib文件夹,我添加了foo.ex
defmodule Foo do
  defstruct [:a, :b]

  defimpl Jason.Encoder do
    def encode(%Foo{a: a, b: b}, opts) do
      Jason.Encode.map(%{"a" => a, "b" => b}, opts)
    end
  end
end
  • 在测试文件夹中,我添加了foo_test.exs
defmodule FooTest do
  use ExUnit.Case

  defmodule Bar do
    defstruct [:c, :d]

    defimpl Jason.Encoder do
      def encode(%Bar{c: c, d: d}, opts) do
        Jason.Encode.map(%{"c" => c, "d" => d}, opts)
      end
    end
  end

  test "encodes Foo" do
    %Foo{a: 1, b: 2} |> Jason.encode!() |> IO.inspect()
  end

  test "encodes Bar" do
    %Bar{c: 5, d: 6} |> Jason.encode!()
  end
end

运行 此测试文件导致“encodes Foo”通过,但“encodes Bar”失败并显示警告

warning: the Jason.Encoder protocol has already been consolidated, an implementation for FooTest.Bar has no effect. If you want to implement protocols after compilation or during tests, check the "Consolidation" section in the Protocol module documentation

后面测试出错

** (Protocol.UndefinedError) protocol Jason.Encoder not implemented for %FooTest.Bar{c: 5, d: 6} of type FooTest.Bar (a struct), Jason.Encoder protocol must always be explicitly implemented.

这是因为协议合并,导致Bar协议无法编译。

您可以在测试环境中关闭协议整合,方法是将以下内容添加到 mix.exs

def project do
  # ...
  consolidate_protocols: Mix.env() != :test,                   
  #...
end

如果这样做,协议将编译并且两个测试都将通过。

但是,解决办法可能就是不直接在测试文件中写入struct/protocol。