michalmuskala / jason

A blazing fast JSON parser and generator in pure Elixir.
Other
1.6k stars 170 forks source link

map -> sort -> keyword list ; encode json #98

Closed randysecrist closed 4 years ago

randysecrist commented 4 years ago

I've been futzing with the use case of rendering json where the keys are sorted via Keyword list. So far this has been one of the only encoding cases I haven't been able to get jason to do

Currently ...

my_map |> Enum.sort |> Enum.flat_map(fn({k,v}) -> ["#{k}": v] end) spits out a keyword list which is sorted by map key ...

However each element is also a Tuple and jason doesn't have a protocol built for handling tuples. I can add a protocol for Tuple but I don't have any way to converting the overall Keyword list with the flat Key/Value list into a single JSON object ...

I'm not exactly clear on how I should implement the protocol for Jason.Encoder .. or how exactly to proceed. Hacking on this I usually end up converting the Keyword list back to a Map which has unsorted values.

I see some recent work was done to add Keyword list support but the test case looks off ... the input is a struct (which is a map).

What are your thoughts on getting keyword lists as a supported term? How should I be going about this if jason is my only encoder/decoder?

randysecrist commented 4 years ago

fyi: for now, my particular project has jsx in the beam path - so I can just pass the KW list to it ... but I'd like to find a jason way of doing this if possible.

OvermindDL1 commented 4 years ago

I can add a protocol for Tuple but I don't have any way to converting the overall Keyword list with the flat Key/Value list into a single JSON object ...

Convert it to a map first then. Keyword lists aren't handled by Jason because they allow duplicates, and JSON object's don't, so you need to deal with it before-hand to make it unambiguous, and thus to convert to a json object then you want to convert the keyword list into a map, which is as simple as just passing the keyword list into Map.new(...).

"#{k}"

Also be very very very careful here, this is just absolutely ripe waiting for an atom exhaustion attack. You don't want to dynamically create atoms unless it's at compile-time only.

my_map |> Enum.sort |> Enum.flat_map(fn({k,v}) -> ["#{k}": v] end)

Also, why are you sorting it, JSON objects are specifically unsorted by the spec and doing otherwise is not following the spec (plus it won't work in this case anyway), thus it's a wasted operation, why not just pass my_map in straight, assuming it is a map? ^.^;

randysecrist commented 4 years ago

Convert it to a map first then. Keyword lists aren't handled by Jason because they allow duplicates, and JSON object's don't, so you need to deal with it before-hand to make it unambiguous, and thus to convert to a json object then you want to convert the keyword list into a map, which is as simple as just passing the keyword list into Map.new(...).

The KW list is assembled prior to this via a series of merged maps (equiv of using a set) so duplicates won't be an issue. I noticed there are options passed around; I haven't used jason long enough to know if it has a strict mode or not but that would be handy to have documented somewhere....?

Also be very very very careful here, this is just absolutely ripe waiting for an atom exhaustion attack. You don't want to dynamically create atoms unless it's at compile-time only.

I get that ... keyspace is constrained however do I'm not worried about atom exhaustion. App is very purpose built and that has all been worked out already; and also not the problem I am solving here.

my_map |> Enum.sort |> Enum.flat_map(fn({k,v}) -> ["#{k}": v] end)

Also, why are you sorting it, JSON objects are specifically unsorted by the spec and doing otherwise is not following the spec (plus it won't work in this case anyway), thus it's a wasted operation, why not just pass my_map in straight, assuming it is a map? ^.^;

I'm sorting the attributes for occasional human consumption; its a requirement in my case to do so & many other encoders support it.

I'm totally fine if jason doesn't want to support it .. I just need to know that so I can adjust as needed.

michalmuskala commented 4 years ago

You can achieve this with a custom struct for the keyword and an encoder implementation. Something like:

defmodule SortedObject do
  defstruct [:value]

  def new(value), do: %__MODULE__{value: value}

  defimpl Jason.Encoder do
    def encode(%{value: value}, opts) do
      Jason.Encode.keyword(Enum.sort(value), opts)
    end
  end
end

## Used as:

Jason.encode!(SortedObject.new(%{a: 1, b: 2}))

As this is possible to do with relatively little code, I'm not planning on adding something built-in to Jason for this at this time.

randysecrist commented 4 years ago

👍 - thanks for the clarification!

adamu commented 4 years ago

@michalmuskala Is there a plan to release the next version to hex? It looks like keyword lists, which you used in your solution above, were pushed a year ago but have never been released to hex.