Skip to content

JSONEncoder cannot encode structs #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
digitalcora opened this issue Jun 29, 2018 · 3 comments
Closed

JSONEncoder cannot encode structs #20

digitalcora opened this issue Jun 29, 2018 · 3 comments

Comments

@digitalcora
Copy link

In the typical case where data being encoded from Phoenix is a struct (using @derive Poison.Encoder or similar), ProperCase.JSONEncoder.CamelCase does nothing. This doesn't seem well-documented in the README, and makes this encoder not particularly useful out of the box, although it does make sense: the struct-ness cannot be preserved if the keys are changed, but it must be preserved in order to be encoded as desired by Poison/Jason/etc.

To work around this, we're using a custom format encoder that encodes the struct, decodes it back into a map (now with filtered/stringified keys), runs that through ProperCase, and re-encodes it. This would probably be inefficient for large JSON responses, but it's acceptable for our application.

defmodule MyApp.JSONEncoder do
  @moduledoc "JSON encoder that camelizes all object keys in the final JSON output."

  def encode_to_iodata!(data) do
    data
    |> Poison.encode!
    |> Poison.decode!
    |> ProperCase.to_camel_case
    |> Poison.encode_to_iodata!
  end
end

I'm not sure off-hand how this issue could be addressed within ProperCase — there is nowhere in e.g. Poison to insert a "key transform" (although see devinus/poison#44). I think this library could at least be explicit in the README that JSONEncoder.CamelCase only works in the unusual case where the data being encoded is plain maps all the way down, with no structs.

@szajbus
Copy link

szajbus commented Aug 28, 2018

It's actually stated in code comments:

If the map is a struct with no Enumerable implementation,
the struct is considered to be a single value.

@steven-cole-elliott
Copy link

steven-cole-elliott commented Aug 7, 2019

@Grantovich I, too, had the same realization as you did, and I'm replying with the hopes that perhaps this helps someone else that stumbles upon this useful library but is at a loss momentarily for how to leverage it.

I agree with your assessment that encoding, then decoding so that you can perform the casing transformations, and then encoding again is something that you'd like to avoid for every request if you could.

Trying to avoid that pattern lead me down this approach, though note I'm using Jason rather than Poison, but I think the implementation would be similar.

In my structs, I've defined a an implementation of Jason.Encoder for my given struct, and inside that encode function, I can now do the work that Jason would have done for me for free - that is taking only the keys that I want - and then do what is necessary to be able to leverage ProperCase.to_camel_case before finally doing the encoding via Jason.

I've only played around with it a bit, but the rough idea is

defmodule MyApp.MyStruct do
  use Ecto.Schema
  import Ecto.Changeset

  alias MyApp.Common

  defimpl Jason.Encoder, for: [MyApp.MyStruct] do
    def encode(struct, opts) do
      map =
        struct
        |> Map.take([:key_1, :key_2])
        |> Common.map_from_struct()
        |> ProperCase.to_camel_case()

      Jason.Encode.map(map, opts)
    end
  end

  schema "my_structs" do
    field :key_1, :integer
    field :key_2, :integer

    timestamps()
  end
end

The call to Common.map_from_struct/1 would take some term and convert all structs in it to maps, no matter the level of nesting, so there's no issue with calling ProperCase.to_camel_case later because all structs will be their map representations.

Though you have to write a bit more code, you have as much control as you want over the encoding behavior, and can perform the casing transformations without having to write your own library to do it.

@digitalcora
Copy link
Author

digitalcora commented Aug 7, 2019

Oops, I think I should have closed this a while ago, as we realized the way we were doing JSON rendering did not (did no longer?) align with Phoenix best practices. Specifically, I asserted that

data being encoded from Phoenix is a struct (using @derive Poison.Encoder or similar)

...was a "typical case", which I'm not sure is accurate. The current Phoenix guides, at least, recommend using view modules in a way that results in plain maps being passed to the encoder, which of course works fine with the ProperCase.JSONEncoder. I would recommend anyone finding this issue in the future to consider this approach, since it also results in much better separation of concerns.

I'll close this now since, for me, it is not an issue. If anyone is in a situation that prevents using view modules for whatever reason, the workarounds documented above should help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants