Search code examples
jsonnet

Conditional in the top level object cannot access merged config


I'm dealing with jsonnet and I cannot access to the $._config coming from the merged object

{
  if ! $._config.hello.enable then {
    'hello': 'world'
  } else {},
} + {
  _config+:: {
    hello: {
      enable: true
    },
  }
}

the error says

unexpected: if while parsing field definition

I'm not sure how different I can represent the top level json in order to get access to the config parameters


Solution

  • This is a syntax error. Object contains fields, not expressions. You cannot just put an if directly in an object literal (what would that even mean?).

    You can do something like that, though:

    {
      // Please note, that even if enable = false, the field is still present, but its value is null.
      'hello': if self._config.hello.enable then 'world',
    } + {
      _config+:: {
        hello: {
          enable: true
        },
      }
    }
    

    Meaning of $

    I think there might be some confusion about semantics of $. It doesn't refer to the result of the whole file, but to lexically outermost object.

    So for example:

    local a = {
      x: "a",
      y: $.x
    };
    local b = {
      x: "b",
      y: $.x
    };
    {
      a: a,
      b: b,
    } 
    

    results in:

    {
      "a": {
        "x": "a",
        "y": "a"
      },
      "b": {
        "x": "b",
        "y": "b"
      }
    }
    

    So basically it's like every time there are curly braces in your code which are not inside another pair of curly braces it wrapped it like that local $ = { ... }; $ (modulo $ not being an ordinarily valid identifier and a slight difference in behavior with comprehensions).

    Removing / hiding the field

    The set of fields and their visibility is determined when the object is created, so you cannot make an object where a presence (or visibility) of a field depends on some other field. There are some other things you can do to similar effect, but the enabled must not be a part or depend on the object to be created, because that would be circular.

    One thing you can do is to modify the object to hide the field, e.g.:

    local enabled = true;
    {hello: "world"} + 
    (if enabled then {} else {hello:: super.hello})
    

    Alternatively, you can also make a presence of a field conditional based on some other value outside of object:

    local enabled = true; { [if enabled then "hello"]: "world" }
    

    Note that in this case the field will not be present at all – it's different from just hiding it. When it's hidden it doesn't get printed, doesn't matter for equality checks etc., but it's still there. When it's not present, then well, it's not there at all.

    A special case of that would be to wrap your object in a function which takes the config instead of having it as part of the object.

    local makeMyObject(config) = {
      [if config.enabled then "hello"]: 
    }
    

    Yet another way would be to have a wrapper object which has both your target object and the config:

    local wrapper = {
      myObject: {
        // Note that self in field name expressions refers to the outer object (wrapper)
        // This makes sense, because they're evaluated before myObject is created.  
        [if self.config.enabled then "hello"]: "world",
      },
      config: {
        enabled: true
      }
    }; wrapper.myObject