Skip to main content

2 posts tagged with "json"

View All Tags

· 4 min read
Mark Stella

Everytime I start a new project I try and optimise how the application can work across multiple envronments. For those who don't have the luxury of developing everything in docker containers or isolated spaces, you will know my pain. How do I write code that can run on my local dev environment, migrate to the shared test and ci environment and ultimately still work in production.

In the past I tried exotic options like dynamically generating YAML or JSON using Jinja. I then graduated to HOCON which made my life so much easier. This was until I stumbled across Jsonnet. For those who have not seen this in action, think JSON meets Jinja meets HOCON (a Frankenstein creation that I have actually built in the past)

To get a feel for how it looks, below is a contrived example where I require 3 environments (dev, test and production) that have different paths, databases and vault configuration.

Essentially, when this config is run through the Jsonnet templating engine, it will expect a variable 'ENV' to ultimately refine the environment entry to the one we specifically want to use.

A helpful thing I like to do with my programs is give users a bit of information as to what environments can be used. For me, running a cli that requires args should be as informative as possible - so listing out all the environments is mandatory. I achieve this with a little trickery and a lot of help from the click package!

local exe = "application.exe";

local Environment(prefix) = {
root: "/usr/" + prefix + "/app",
path: self.root + "/bin/" + exe,
database: std.asciiUpper(prefix) + "_DB",
tmp_dir: "/tmp/" + prefix
};

local Vault = {
local uri = "http://127.0.0.1:8200/v1/secret/app",
_: {},
dev: {
secrets_uri: uri,
approle: "local"
},
tst: {
secrets_uri: uri,
approle: "local"
},
prd: {
secrets_uri: "https://vsrvr:8200/v1/secret/app",
approle: "sa_user"
}
};

{

environments: {
_: {},
dev: Environment("dev") + Vault[std.extVar("ENV")],
tst: Environment("tst") + Vault[std.extVar("ENV")],
prd: Environment("prd") + Vault[std.extVar("ENV")]
},

environment: $["environments"][std.extVar("ENV")],
}

The trick I perform is to have a placeholder entry '_' that I use to initially render the template. I then use the generated JSON file and get all the environment keys so I can feed that directly into click.

from typing import Any, Dict
import click
import json
import _jsonnet
from pprint import pprint

ENV_JSONNET = 'environment.jsonnet'
ENV_PFX_PLACEHOLDER = '_'

def parse_environment(prefix: str) -> Dict[str, Any]:
_json_str = _jsonnet.evaluate_file(ENV_JSONNET, ext_vars={'ENV': prefix})
return json.loads(_json_str)

_config = parse_environment(prefix=ENV_PFX_PLACEHOLDER)

_env_prefixes = [k for k in _config['environments'].keys() if k != ENV_PFX_PLACEHOLDER]


@click.command(name="EnvMgr")
@click.option(
"-e",
"--environment",
required=True,
type=click.Choice(_env_prefixes, case_sensitive=False),
help="Which environment this is executing on",
)
def cli(environment: str) -> None:
config = parse_environment(environment)
pprint(config['environment'])


if __name__ == "__main__":
cli()

This now allows me to execute the application with both list checking (has the user selected an allowed environment?) and the autogenerated help that click provides.

Below shows running the cli with no arguments:

$> python cli.py

Usage: cli.py [OPTIONS]
Try 'cli.py --help' for help.

Error: Missing option '-e' / '--environment'. Choose from:
dev,
prd,
tst

Executing the application with a valid environment:

$> python cli.py -e dev

{'approle': 'local',
'database': 'DEV_DB',
'path': '/usr/dev/app/bin/application.exe',
'root': '/usr/dev/app',
'secrets_uri': 'http://127.0.0.1:8200/v1/secret/app',
'tmp_dir': '/tmp/dev'}

Executing the application with an invalid environment:

$> python cli.py -e prd3

Usage: cli.py [OPTIONS]
Try 'cli.py --help' for help.

Error: Invalid value for '-e' / '--environment': 'prd3' is not one of 'dev', 'prd', 'tst'.

This is only the tip of what Jsonnet can provide, I am continually learning more about the templating engine and the tool.

if you have enjoyed this post, please consider buying me a coffee ☕ to help me keep writing!

· 5 min read
Jeffrey Aven

JSON Golang

Golang is a fantastic language but at first glance it is a bit clumsy when it comes to JSON in contrast to other languages such as Python or Javascript. Having said that once you master the concepts involved with JSON wrangling using Go it is equally as functional – with added type safety and performance.

In this article we will build a program in Golang to parse a JSON file containing a collection held in a named key – without knowing the structure of this object, we will expose the schema for the object including data types and recurse the object for its values.

This example uses a great Go package called tablewriter to render the output of these operations using a table style result set.

The program has describe and select verbs as operation types; describe shows the column names in the collection and their respective data types, select prints the keys and values as a tabular result set with column headers for the keys and rows containing their corresponding values.

Starting with this:

We will end up with this when performing a describe operation:

And this when performing a select operation:

Now let’s talk about how we got there…

The JSON package

Support for JSON in Go is provided using the encoding/json package, this needs to be imported in your program of course… You will also need to import the reflect package – more on this later. io/ioutil is required to read the data from a file input, there are other packages included in the program that are removed for brevity:

Reading the data…

We will read the data from the JSON file into a variable called body, note that we are not attempting to deserialize the data at this point. This is also a good opportunity to handle any runtime or IO errors that occur here as well.

The interface…

We will declare an empty interface called data which will be used to decode the json object (of which the structure is not known), we will also create an abstract interface called colldata to hold the contents of the collection contained inside the JSON object that we are specifically looking for:

Validating…

Next we need to validate that the input is a valid JSON object, we can use the json.Valid(body) method to do this:

Unmarshalling…

Now the interesting bits, we will deserialize the JSON object to the empty data interface we created earlier using the json.Unmarshal() method:

Note that this operation is another opportunity to catch unexpected errors and handle them accordingly.

Checking the type of the object using reflection…

Now that we have serialized the JSON object into the data interface, there are several ways we can inspect the type of the object (which could be a map or an array). One such way is to use reflection. Reflection is the ability of a program to inspect itself at runtime. An example is shown here:

This instruction would produce the following output for our zones.json file:

The type switch…

Another method to decode the type of the data object (and any objects nested as elements or keys within the data object), is to use the type switch, an example of a type switch function is shown here:

Finding the nested collection and recursing it…

The aim of the program is to find a collection (an array of maps) nested in a JSON object. The maps with each element of the array are unknown at runtime and are discovered through recursion.

If we are performing a describe operation, we only need to parse the first element of the collection to get the key names and the data type of the values (for which we will use the same getObjectType function to perform a type switch.

If we are performing a select operation, we need to parse the first element to get the column names (the keys in the map) and then we need to recurse each element to get the values for each key.

If the element contains a key named id or name, we will place this at the beginning of the resultant record, as maps are unordered by definition.

The output…

As mentioned, we are using the tablewriter package to render the output of the collection as a pretty printed table in our terminal. As wrap around can get pretty ugly an additional maxfieldlen argument is provided to truncate the values if needed.

In summary…

Although it is a bit more involved than some other languages, once you get your head around processing JSON in Go, the possibilities are endless!

Full source code can be found at: https://github.com/gamma-data/json-wrangling-with-golang

if you have enjoyed this post, please consider buying me a coffee ☕ to help me keep writing!