jsonnet icon indicating copy to clipboard operation
jsonnet copied to clipboard

Remove a field from an object

Open ant31 opened this issue 8 years ago • 25 comments

I haven't found examples nor a way to easily remove a field from an object.

I can eventually iterate over all key and skip the one I want to remove to build a new object with dict comprehension, but the object would lost hidden fields.

ant31 avatar May 26 '17 13:05 ant31

std.objectFieldsAll(o) will return the hidden ones too. However all your hidden fields would become visible because of the : in { [f]: v for f in std.objectFieldsAll(o) }

sparkprime avatar May 26 '17 18:05 sparkprime

Can you get the effect of removing a field just by marking it as hidden? x :: super.x

sparkprime avatar May 26 '17 18:05 sparkprime

Is there any chance that dict comprehension can include to use hidden too?

ant31 avatar Jun 04 '17 19:06 ant31

One thing I'd like to do is generalize the object comprehension and merge it with the object literal. This means things like this will be possible:

local fields = std.range(1, 100);
{
    ["field" + i]: true for i in fields if i % 2 == 0,
    ["field" + i]:: true for i in fields if i % 2 == 1,
    lots_of_fields: true if std.length(fields) > 50,
}

There's another request in #90 to extend comprehensions.

What you can do is then:

{
    [k]: obj[k] for k in std.objectFields(obj),
    [k]:: obj[k] for k in std.objectFieldsAll(obj) if !std.objectHas(obj, k),
}

sparkprime avatar Jun 04 '17 20:06 sparkprime

It's quite easy from that to build it with all fields except one, just put if k != "foo" at the end.

sparkprime avatar Jun 04 '17 21:06 sparkprime

This isn't strictly the same as removing a field from an object though, what it's doing is creating a statically bound copy. E.g. if we took the above code and put it in a function clone(), then:

local obj = {x: 1, y: self.x * 2};
{
    obj1: obj {x: 10},
    obj2: clone(obj) {x: 10},
}

would yield:

{
    "obj1": { "x": 10, "y": 20 },
    "obj2": { "x": 10, "y": 2 }
}

sparkprime avatar Jun 04 '17 21:06 sparkprime

@sparkprime Yes, extending object comprehension and with hidden field would solve this issue. as you suggested:

objectPop(obj, key): { 
    [k]: obj[k] for k in std.objectFields(obj) if k != key, 
    [k]:: obj[k] for k in std.objectFieldsAll(obj) if !std.objectHas(obj, k) && k != key,
 } 

ant31 avatar Jun 08 '17 17:06 ant31

@sparkprime Yes, extending object comprehension and with hidden field would solve this issue. as you suggested:

objectPop(obj, key): { 
    [k]: obj[k] for k in std.objectFields(obj) if k != key, 
    [k]:: obj[k] for k in std.objectFieldsAll(obj) if !std.objectHas(obj, k) && k != key,
 } 

Unfortunately, the object is not late bound anymore in this case.

mbarbero avatar May 07 '20 13:05 mbarbero

@mbarbero Yes. { [k]: obj[k] for k in std.objectFieldsAll(obj) } is not the same as obj. It is merely an object which has all the same values in the same fields. But self is fixed as this point (and super is affected as well).

That said, objectPop solution is still useful for objects which just hold data and don't use self or super.

sbarzowski avatar May 07 '20 14:05 sbarzowski

Indeed, it's still useful. My comment was a bit "raw", sorry about that. I just wanted to note that, if jsonnet ever includes a solution to remove fields from an object, it would be great if the solution would retain the late bounding on target object.

mbarbero avatar May 07 '20 14:05 mbarbero

I've just realized that the proposed implementation of objectPop doesn't actually work. You can't have hidden fields in object comprehensions nor multiple fors there. So the best you can do AFAICT is something like:

objectPop(obj, key): { 
    [k]: obj[k] for k in std.objectFieldsAll(obj) if k != key
} 

And that loses the visibility information.

sbarzowski avatar May 07 '20 23:05 sbarzowski

I just ran into this, finding that replicas: null is not the same as excluding the replicas field in the manifest in kubernetes for a deployment object.

If the # of replicas is managed by a Horizontal Pod Autoscaler, then you want to ensure that you don't change the # of replicas each time you deploy.

ghostsquad avatar Jan 22 '21 20:01 ghostsquad

could you do something like:

objectPop(obj, key): {
    if std.objectHas(obj, k) then
       [k]: obj[k]
    else
       [k]:: obj[k]
    for k in std.objectFieldsAll(obj) if k != key
} 

?

ghostsquad avatar Jan 25 '21 05:01 ghostsquad

Not really. The comprehensions only support a single field pattern. Also you can't wrap the fields in ifs like that. If you have a proposition how that could work, we can look into it. Right now you can do it with two comprehensions (one for : and the other for ::) and +.

Perhaps a more direct way of handling this, would be to just add a builtin to remove a field. We would need to specify what it means for self and super. I'm also open to proposals.

Generally, I would recommend removing fields only for "plain" objects anyway and you care only about what fields evaluate to and not automagically maintaining relationships between them. By plain objects I mean objects where each field is visible and concrete (i.e. doesn't depend on other fields through self/super). In such case objectPop is doing what you want.

sbarzowski avatar Jan 25 '21 11:01 sbarzowski

I really expected something like CSS's property: unset here, all of the above just feels like complex workarounds.

DieterVDW avatar Feb 10 '22 11:02 DieterVDW

Years ago I thought about adding the facility to remove a field in a mixin. The syntax might look like that, or maybe { null f } or something.

sparkprime avatar Feb 10 '22 13:02 sparkprime

Right now you can do it with two comprehensions (one for : and the other for ::) and +.

What do you mean right now? I can't do :: at all in object comprehensions, right? ("cannot have hidden fields").

frimik avatar Jun 15 '22 10:06 frimik

@frimik Oh, I'm afraid you're right.

sbarzowski avatar Jun 18 '22 19:06 sbarzowski

Deconstruction proposal partially (this/super becomes frozen) solves this issue: https://github.com/google/jsonnet/issues/307

local sourceObject = {
  a: 1,
  b: 2,
  c:: 3,
  fieldToOmit: 4,
};

local {
  fieldToOmit: ?,
  ...targetObject
} = sourceObject;

std.manifestJson(targetObject) == '{"a": 1, "b": 2}'

CertainLach avatar Jun 25 '22 15:06 CertainLach

I started using [k]: null in an object comprehension + std.prune().

Now I changed methods to using std.mergePatch() instead which makes it slightly cleaner in my particular case. According to #216 - beware that object gets resolved to JSON at that point?

    local ddMergePatch = {
      [k]: null
      for k in std.objectFields(agent.datadog)
      if std.objectHas(agent.datadog[k], 'kind') && agent.datadog[k].kind != 'PodSecurityPolicy'
    };

    local agentMergePatch =       {
        [k]: null
        for k in std.objectFields(agent)
        if std.objectHas(agent[k], 'kind') && agent[k].kind != 'PodSecurityPolicy'
      };

    std.mergePatch(agent, agentMergePatch) {
      datadog: std.mergePatch(agent.datadog, ddMergePatch),
    },

frimik avatar Jun 29 '22 23:06 frimik

Right now you can do it with two comprehensions (one for : and the other for ::) and +.

What do you mean right now? I can't do :: at all in object comprehensions, right? ("cannot have hidden fields").

I saw Jrsonnet mentioned in this thread, and they do support this:

./target/release/jrsonnet -e "local a = {[i[0] + '!']:: i[1] + '!' for i in {a: 1,b: 2,c: 3,}}; a['a!']"
"1!"

You do have to build it yourself though to get this functionality, but looks promising.

dxlr8r avatar Dec 12 '22 12:12 dxlr8r

Also, on the topic of object comprehensions and for loops. Recursive functions does not have this issue. I ended up creating my own forEach/map function:

https://github.com/dxlr8r/dxsonnet/blob/main/lib/obj.libsonnet

With a function like that in place, it is simple to create for example a pop function, which can then be called like this:

local obj = import "../lib/obj.libsonnet";
local x = {a: 0, b: 1, c: 2, d:: 3}; 

{
  // remove field `a` from obj `x`
  pop: obj.pop(x, "a"), // returns: `{b: 1, c: 2, d:: 3}`
}

dxlr8r avatar Dec 12 '22 13:12 dxlr8r

Hey there,

I went with this approach which allows multiple fields to be ignored.

In my filter.libsonnet file I got this:

{
  // Function to filter an object by excluding specified fields.
  // Parameters:
  // - inputObject: The object to be filtered.
  // - fieldsToIgnore: List of fields to be ignored from the input object.
  filterObjectFields(inputObject, fieldsToIgnore):: 
    // Iterating over the fields in the input object and creating a new object 
    // without the fields specified in `fieldsToIgnore`.
    std.foldl(function(filteredObject, currentField) 
      // If current field is in `fieldsToIgnore`, return the filtered object as is.
      // Otherwise, add the current field to the filtered object.
      (
        if std.member(fieldsToIgnore, currentField) then
          filteredObject
        else
          filteredObject + { [currentField]: inputObject[currentField] }
      ),
      // Starting with an empty object and iterating over each field in the input object.
      std.objectFields(inputObject), {}),
}

Then you can use it like:

local filter = import 'filter.libsonnet';

local fieldsToIgnore = [
  'ignore1',
  'ignore2',
  'ignore3',
];

local objectToFilter = {
  'field1': 'value1',
  'field2': 'value2',
  'ignore1': 'value3',
  // ...
};

// Filter object
filter.filterObjectFields(objectToFilter, fieldsToIgnore)

Works well for my use case but might not be the most optimal solution out there. Maybe that helps someone.

icereed avatar Jun 01 '23 09:06 icereed

If someone wants to add the ability to do :: in an object comprehension then please send a PR (go-jsonnet only is fine) Ditto for +: and +:: (neither of which we support)

On Thu, 1 Jun 2023 at 10:25, Icereed @.***> wrote:

Hey there,

I went with this approach which allows multiple fields to be ignored.

In my filter.libsonnet file I got this:

{ // Function to filter an object by excluding specified fields. // Parameters: // - inputObject: The object to be filtered. // - fieldsToIgnore: List of fields to be ignored from the input object. filterObjectFields(inputObject, fieldsToIgnore):: // Iterating over the fields in the input object and creating a new object // without the fields specified in fieldsToIgnore. std.foldl(function(filteredObject, currentField) // If current field is in fieldsToIgnore, return the filtered object as is. // Otherwise, add the current field to the filtered object. ( if std.member(fieldsToIgnore, currentField) then filteredObject else filteredObject + { [currentField]: inputObject[currentField] } ), // Starting with an empty object and iterating over each field in the input object. std.objectFields(inputObject), {}), }

Then you can use it like:

local filter = import 'filter.libsonnet'; local fieldsToIgnore = [ 'ignore1', 'ignore2', 'ignore3', ]; local objectToFilter = { 'field1': 'value1', 'field2': 'value2', 'ignore1': 'value3', // ... }; // Filter object filter.filterObjectFields(objectToFilter, fieldsToIgnore)

Works well for my use case but might not be the most optimal solution out there. Maybe that helps someone.

— Reply to this email directly, view it on GitHub https://github.com/google/jsonnet/issues/312#issuecomment-1571677829, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABJBXTOBJVRZVUMZ47E4ZLXJBNXDANCNFSM4DM6NSPA . You are receiving this because you were mentioned.Message ID: @.***>

sparkprime avatar Jun 07 '23 11:06 sparkprime

(in response to https://github.com/google/jsonnet/issues/312#issuecomment-1346507914)

as a simpler workaround to manually rolling recursive functions, fold can be used:

local hide(obj, field=null, pred=function(name) (name == field), map=function(value) value) =
  std.foldl(
    function(acc, f)
      if pred(f.key) then
        acc { [f.key]:: map(f.value) }
      else
        acc { [f.key]: f.value },
    std.objectKeysValues(obj),
    {}
  );

(code with tests https://gist.github.com/mkmik/ba6110b5fa23352f9c9304a6aa51e614)

of course, if one only wants to hide a field with a given name, that's just

local hide(obj, field) = obj { [field]:: std.get(obj, field) };

the object-comprehension is needed if you want to apply some predicates and/or map the values.

(unrelated note: I was pleasantly surprised when I learned that you can have a default argument of a function depend on another argument of the function, e.g. above in the function(name) (name == field) references the previous arg field )

mkmik avatar Sep 06 '23 04:09 mkmik