This page is a guided tour through the Jsonnet language, from its most basic features to its powerful object model, punctuated with examples drawn from the world of cocktails. These examples are meant to be fun, and although a little contrived, do not restrict our thinking to any one particular application of Jsonnet.
Caveat: Note that Jsonnet unparses JSON in a simple way. In particular, it alphabetically reorders object fields in its output. This is natural and compatible with JSON, since if order is meaningful, an array of pairs should be used instead of an object. Also, unparsing JSON using a canonical ordering of field names makes it possible to use diff to compare outputs. However, the example output on this page has been manually re-ordered in order to allow easier visual comparison to the given input. The whitespace of the output has also been tweaked to make it fit more neatly on the page. So, if you run these examples yourself, the output might be different (but equivalent).
In Jsonnet, unlike JSON, sufficiently simple object fields (the strings to the left of the colon) do not need quotes if their names are valid identifiers (i.e. match the regular expression [a-zA-Z_][a-zA-Z0-9_]*). In JSON, commas are not allowed at the end of arrays / objects, which can make writing JSON painful if you need to reorder an array or delete the last element. In Jsonnet, extra commas are allowed but not required. Jsonnet also allows both C and C++ style comments.
Input (Jsonnet) | Output (JSON) |
|
|
To avoid duplication, one part of the structure can refer to another part. There is a keyword
self
that refers to the current object, i.e. the object whose braces we are immediately
inside. There is also an operator $
(syntax borrowed from JsonPath) that refers to the root object, i.e. the
object whose braces we are inside that is most removed from us by nesting. From one of these, we
can follow a path to the other part of the structure, which contains the value we want to
reference.
In the example below, we re-use the spirit from the Tom Collins cocktail when defining the recipe for a Martini. In the path to the spirit, we need to look up fields and array indexes. In both cases, square brackets are used to specify which element or field is being looked up. For objects, fields that are valid identifiers (e.g. have no spaces) can be traversed using the dot operator which is a more convenient notation. The commented out line shows what the path would look like if the dot operator was not taken advantage of.
The Gin Martini is another name for a Martini, so an alias has been added by using
self
, which resolves to the closest surrounding object (in this case, the "cocktails"
object).
Input (Jsonnet) |
|
Output (JSON) |
|
Jsonnet has constructs for manipulating data. There are the usual arithmetic operators on numbers (double precision floating point), booleans, and strings. The + operator acts on arrays and strings to concatenate them. It also acts on objects by fusing the two objects, i.e. yielding a new object with all of the fields and values from both. In the event that the two objects both define the same field, then the resulting object will use the value from the object that was on the right hand side of the +.
There is an if
construct for conditional code. Since Jsonnet is an
expression language, the conditional behaves more like the ternary "?:" operator in C, or the "a if
b else c" syntax of Python, in that it can be embedded in the middle of an expression.
The example below illustrates some of these features. Note that equality returns false if the two values have different types, as is the case in most dynamically typed scripting languages (but not Javascript).
Input (Jsonnet) | Output (JSON) |
|
|
There are constructs for array and object comprehension, i.e. creating a list or object by
processing each element of another list. The expression before the for
is
evaluated once for each element in the array after the in
, but only if the
condition after if
holds. The syntax matches Python. Also, field names can
be computed in regular object definitions as well, as shown on the last line of the following
example.
Input (Jsonnet) | Output (JSON) |
|
|
The next example is less contrived. The first cocktail has equal parts of three ingredients so we use an array comprehension to avoid repeating ourselves. For the sake of example, the qty is also computed as 4/3 instead of being given as 1.3333...
After the +, object comprehension is used to create two more cocktails. In this case, the prefix
in front of "Screwdriver" and the kind of fruit juice is what differs each time. Note also the use
of null
, which is a special value that we have inherited from JSON. The + fuses the two objects
together to produce a single record for the "cocktails" field.
Input (Jsonnet) | Output (JSON) |
|
|
As the amount of JSON grows, its size makes it harder to manage. Jsonnet has various constructs to help. A file can be broken into parts, as one Jsonnet file can import other Jsonnet files (and therefore other JSON files). Values can be held in local variables and fields, which are only visible within their scopes. Functions can be defined to factor out common descriptions, and error statements can be used to validate inputs. Jsonnet provides a standard library that is implicitly imported and contains useful functions for data manipulation, among other things.
The first example below factors out some cocktails into a separate file. This may be useful to
allow concurrent modifications by different mixologists. The import
construct yields
the content of the martinis.jsonnet file. The +
operator is object
concatenation, which combines two objects to form a single object. Note that the Cosmopolitan field
is defined in both files, so the one on the right hand side is used. This means that
bar_menu.jsonnet has overridden the recipe from martinis.jsonnet with a different
recipe (one that uses Cointreau instead of Triple Sec, among other changes).
// martinis.jsonnet
{
"Vodka Martini": {
ingredients: [
{ kind: "Vodka", qty: 2 },
{ kind: "Dry White Vermouth", qty: 1 },
],
garnish: "Olive",
served: "Straight Up",
},
Cosmopolitan: {
ingredients: [
{ kind: "Vodka", qty: 2 },
{ kind: "Triple Sec", qty: 0.5 },
{ kind: "Cranberry Juice", qty: 0.75 },
{ kind: "Lime Juice", qty: 0.5 },
],
garnish: "Orange Peel",
served: "Straight Up",
},
}
// bar_menu.6.jsonnet
{
cocktails: import "martinis.jsonnet" + {
Manhattan: {
ingredients: [
{ kind: "Rye", qty: 2.5 },
{ kind: "Sweet Red Vermouth", qty: 1 },
{ kind: "Angostura", qty: "dash" },
],
garnish: "Maraschino Cherry",
served: "Straight Up",
},
Cosmopolitan: {
ingredients: [
{ kind: "Vodka", qty: 1.5 },
{ kind: "Cointreau", qty: 1 },
{ kind: "Cranberry Juice", qty: 2 },
{ kind: "Lime Juice", qty: 1 },
],
garnish: "Lime Wheel",
served: "Straight Up",
},
}
}
The next example demonstrates functions (actually, closures). We have a separate file holding a utility function to help with defining cocktails made from equal parts, such as the Bee's Knees and the Negroni. The utility function also checks the number of ingredients and raises an error if the list is empty. This avoids the low level divide by zero error that would be raised when calculating the quantity, thus avoiding the exposure of implementation details.
There is also an identity function defined. This is there to demonstrate that function definitions are really just syntax sugar for closures that are assigned to a field.
// bar_menu_utils.jsonnet
{
equal_parts(size, ingredients)::
if std.length(ingredients) == 0 then
error "No ingredients specified."
else [
{ kind: i, qty: size/std.length(ingredients) }
for i in ingredients
],
id:: function(x) x,
}
Finally, you may have noticed that two colons are used instead of one. This marks the field as being hidden, i.e. it will not appear in the JSON output. The field can still be accessed by Jsonnet code, so it is not like the private/protected modifier that some languages have. Hidden fields are convenient for functions, which cannot be manifestated in the JSON output. It has other uses too, as we will see in the later section on object orientation.
Like most languages, Jsonnet has variables, which can be used to factor out
expressions. They are referenced using standard static scoping rules. In the following
case, the imported object is stored in the variable, which is later referenced to access the
equal_parts
function. It is also possible to store the import in a field of the root
object, and access it with $
. In that case, it should be a hidden field (using the ::
syntax) in order to avoid appearing in the output of bar_menu.7.jsonnet
.
Input | Output |
|
|
Variables can appear anywhere within the program structure; in particular, they can appear inside
an object (i.e. alongside field declarations). The my_gin
variable is an example of
using local
inside an object. In this situation they are analogous to "private"
fields, as defined in other languages. This is because the initializer of the variable (in this
case the string "Farmer's Gin"
, but in general an arbitrary expression) can access
self
and super
, just like a field. However, unlike with fields, it is not
possible for anyone to access the variable except by name, within the object's scope.
In both cases, there is a separator that indicates the end of the variable initializer. If the variable is defined alongside object fields, the separator is a comma in order to match the regular field separator. Otherwise the separator is a semicolon.
The variable can be referenced within its own initializer, which is essential for writing recursive functions.
Variables offer a more general alternative to the $
operator, by allowing the
stashing of the self
value. This can be useful to "name" an object in the middle of
the tree, because the path from $
might be long. $
is actually equivalent
to a stashing self
in a variable at the outermost object.
Input | Output |
|
|
Jsonnet provides stack traces when an error is raised. Here is an example, where we provide an
empty list of ingredients to the equal_parts
function.
Input | Output |
|
RUNTIME ERROR: No ingredients specified. bar_menu_utils.jsonnet:5:13-45 function <anonymous> no_ingredients.jsonnet:3:1-24 |
Finally, we see how Jsonnet provides the abstraction mechanisms of object oriented programming, to allow the writing of base templates that can be extended for a variety of purposes.
The object concatenation operator +
can be used to override fields from one object
with another. This is similar to the concept of inheritance in object-oriented languages such as
C++ and Java. It can be used to derive variants from a single template. In the following example,
a Whiskey Sour with egg white is derived from the original Whiskey Sour.
The super
keyword, as in Java, allows access to the object being derived from, i.e.
the object on the left hand side of the +
operator. In this case, it is being used to
fetch the original ingredients of the whiskey sour, so that a 4th ingredient can be added. Removing
the super.ingredients +
would result in a cocktail containing only egg white.
Input | Output |
|
|
The key to making Jsonnet object-oriented is that the self
keyword be "late bound".
This is illustrated in the next example, where an alternative menu is derived from the above. It
takes the original menu, and replaces the cocktails object with a new object that is in turn based
on the original menu's cocktail object. But this new cocktail object overrides the Whiskey sour,
changing Bourbon to Scotch (among other things). The effect of this is not just to replace the
whiskey sour but also to change how the self
keyword behaves in the original menu.
This results in the egg variant now being derived from the new whiskey, because that is now what
self["Whiskey Sour"]
resolves to. This is classic object-orientation in action, and it
is very powerful.
Note also that this example makes use of two syntax sugars (shorthands). The first is that the
object concatenation +
was omitted. This is allowed when it is followed by an opening
brace, so in fact this same simplification could have also been made in all of the previous examples
utilizing the object concatenation operator +
. The second syntax sugar is the
f +: e
operator, which is a little like the += operator from other languages. Its
behavior is the same as saying f: super.f + e
. This works not just for +
when used for inheritance, but also for string concatenation, list concatenation, and arithmetic
addition, so it could also have been used in the previous example to add the egg white.
Input | Output |
|
|
The hidden status (i.e. ::
) of a field is preserved over inheritance. If you
override a field using the :
syntax, but the original field was defined with
::
, then the new object's field will also be hidden. To make the field visible use
three colons (:::
). This is illustrated below. The values are all inherited without
change, but the use of colons changes the visibility of fields in the case of x
and
w
. The x
field is hidden by foo
because of the double
colons. The y
field remains hidden in foo
because the single colon
inherits the hidden status from Base
. The triple colon in z
however
overrides the hidden status from Base
to make the field visible in foo
.
Input | Output |
|
|
In order to support objects whose keys are unknown until run-time, Jsonnet has a syntax allowing the field name to be computed, or omitted entirely. The syntax is similar to object comprehension, which is also a kind of computed field.
Input | Output |
|
|
Except as noted, this content is licensed under Creative Commons Attribution 2.5.