jsonnet
jsonnet copied to clipboard
Remove a field from an object
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.
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) }
Can you get the effect of removing a field just by marking it as hidden? x :: super.x
Is there any chance that dict comprehension can include to use hidden too?
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),
}
It's quite easy from that to build it with all fields except one, just put if k != "foo" at the end.
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 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,
}
@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
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.
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.
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.
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.
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
}
?
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.
I really expected something like CSS's property: unset here, all of the above just feels like complex workarounds.
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.
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 Oh, I'm afraid you're right.
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}'
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),
},
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.
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}`
}
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.
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 infieldsToIgnore, 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: @.***>
(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 )