michalmuskala / jason

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

feature request: Filtering function for maps #145

Closed ewildgoose closed 2 years ago

ewildgoose commented 2 years ago

Hi, the end effect I want to achieve is to drop values from my struct which are nil. Or perhaps more accurately, I'm using Ecto as a validation layer on some JSON input/outputs and some fields are optional and it's complicated/incorrect/confusing in my specific situation if the encoder writes some of these optional keys to the output with nils.

I think it might be useful to have an option in @derive Jason.Encoder() to be able to pass a function, which would complement only: / exclude: and perhaps call Map.filter (new in 1.13 a few hours ago!). Or I guess we could use Enum.filter against the resulting list for backwards compat?

Do you see this as a valuable feature?

In the meantime, do you have a suggestion on how to write a generic replacement function for @derive Jason.Encoder ? I have quite a few calls to it (30 ish). I can see the original macro implementation, but it's not clear to me how to most simply knock up a wrapper that would call a generic function that calls Map.filter and then cascades to the original Jason.Encode.map? I would want to have the option to pass an :exclude param. Do I need a macro to do this? I can't quite see how I could write a generic function in one place and then pass this into each of my modules and pass down the :exclude? Would someone have a minute to demonstrate a skeleton please?

michalmuskala commented 2 years ago

The main advantage of using the derived implementation is that it pre-computes some things at compile-time. But with dynamic filtering this wouldn't be possible.

One way to do it, is to explicitly implement the protocol with something like:

defimpl Jason.Encode, for: [Struct1, Struct2, ...] do
   def encode(struct, opts) do
      to_encode = for {key, value} <- Map.to_list(stuct), value != nil, key != :__struct__, do: {key, value}
      Jason.Encoder.keyword(to_encode, opts)
   end
end

I believe this should be close to the most efficient implementation possible