normalizr
normalizr copied to clipboard
Cannot normalize an array itself? Per-element is not optional?
I am a longtime user of Normalizr on a few projects. I recently upgraded an old project that had a large number (30+) custom normalizations from version 1.1.1 into a more recent version and fell victim to an issue with array normalization. Specifically, there is no way to normalize an array other than to normalize per-element, per lib/normalizr.rb
:
def process(obj, name, options)
if Array === obj
obj.map { |item| process(item, name, options) }.tap do |ary|
ary.compact! if name == :blank
end
else
find(name).call(obj, options)
end
end
This project had many years of adding its own array_of_foobar
normalizations to account for the lack of array support in the past. Thus the feature is very nice to have! In some ways! However the implementation details means that some of the possibilities for normalizing arrays which were possible before are impossible to achieve now.
Arrays are forced to normalize per element, even if this is not desired.
What? Why does this matter?
A simple case in this project was forcing uniqueness in an array:
add :set_uniqueness do |values, options|
options ||= {}
stringify = options.fetch(:stringify, false)
if stringify
values.flatten.map(&:to_s).uniq
else
values.flatten.uniq
end
end
Asserting that a list of values contains no duplicates is, in my mind, a valid task for "normalization". However, with the update, this is not possible. The process
method detects and array and tries to run this on every element. If a model were to use this normalization, it would either (a) not function correctly and raise error such as NoMethodError
on flatten
, or (b) produce a double-wrapped array (e.g. [["foo"], ["bar"]]
, depending how input was structured (I have run into both of these outcomes).
There is (as best I can tell) no way to write a normalization that operates in the entire context of the array itself. Each normalization function is constrained to only consider the single element--not the whole array, even if that is the goal.
What can be done?
I realize it has been six years since Normalizr gained this automatic array functionality. It is unreasonable for me to come in and say "Please change this default and break everyone's expectations to meet my use case!" I completely understand that.
However, it feels strange that there is no way to opt-out of the behavior that forces per-element application of normalization functions when an array is passed to an attribute. Furthermore, I see that the current behavior (automatic array detection) is desirable in many cases!
I can imagine a few ways to implement a solution to this that provide (a) a way to operate on array with its full context and (b) is backwards compatible:
- Allow passing a per-normalization option such as
auto_array: (false|true)
, oreach_element: (false|true)
, or something similarly named, to signal that the normalizers should be passed the full value even if the value is an array. The default beingtrue
would maintain existing behavior. - A library-wide configuration such as
automatic_array_normalization: (false|true)
that would disable the automatic per-element processing for arrays entirely. - Both! In case of a full library setting that disables array processing like
automatically_detect_arrays: false
, one could still provide the per-attributenormalize :favorite_colors, auto_array: true
and opt-in to automatic array processing in that one case. Or ifautomatically_detect_arrays: true
then individual attributes could be opted out byauto_array: false
or similar.
Summary
I feel strongly that operating on an array as a whole is a valid type of normalization. I would be happy to develop a solution that reflects any of these and provide a pull request, as long as any of these proposals match the project vision. This is a tightly-focused library that does what it does, it does so very well, and I appreciate using it for as long as I have. I would welcome contributing back to this project.