# Why JSON Schema?
Your configuration is dangerous if you can't constrain it. Runtime errors could happen any time, causing anywhere from small to catastrophic damage. Remember that most of the time, configuration is only used in some execution pathways. It may be days before you notice a problem (or worse, you never do).
The most important feature of App Config is its validation of these config values.
We've noticed that (especially in the Node.js ecosystem) many packages define their own (opens new window) validation (opens new window) mechanism (opens new window). This is overwhelming (opens new window) and a lot (opens new window) to learn (opens new window).
Instead, we chose JSON Schema (opens new window). It's extremely popular and well supported in many languages. It's used in a lot of popular tools (ie. OpenAPI), and simple to understand. While it can be a little verbose (less so in YAML), it's comprehensive and easy to find resources for.
# The Schema File
App Config uses one singular schema file when running.
This file is located in .app-config.schema.{yml|toml|json|json5}
.
Again, it's entirely up to you what format to use.
Note that App Config will expect the schema file to exist in your current working directory.
The CLI and Node.js API both have options to override the directory, if you need to.
In the CLI, specify -C ./my-dir
. In Node.js, pass { directory: './my-dir' }
when calling loadConfig
.
The schema file should be a normal JSON Schema object.
We do allow you to omit $schema
and $id
if you want to - we choose reasonable defaults for you.
Before diving in, we'll take a look at a typical schema file:
.app-config.schema.json
{
"type": "object",
"additionalProperties": false,
"required": ["server"],
"properties": {
"server": {
"description": "Properties of our HTTP server",
"type": "object",
"additionalProperties": false,
"required": ["port"],
"properties": {
"port": {
"description": "Port that our HTTP server will listen on",
"$ref": "#/definitions/IpPort"
}
}
}
},
"definitions": {
"IpPort": {
"type": "integer",
"minimum": 0,
"maximum": 65535
}
}
}
We chose JSON here, but could use TOML:
.app-config.schema.toml
type = 'object'
additionalProperties = false
required = ['server']
[properties.server]
description = 'Properties of our HTTP server'
type = 'object'
additionalProperties = false
required = ['port']
[properties.server.properties.port]
description = 'Port that our HTTP server will listen on'
"$ref" = '#/definitions/IpPort'
[definitions.IpPort]
type = 'integer'
minimum = 0
maximum = 65535
The two examples are exactly the same, from App Config's point of view.
# JSON Schema Basics
We can't really document JSON Schema here. You're best off to learn from existing resources (opens new window).
We will outline a couple basic techniques here though, with examples. Note that App Config supports the latest JSON Schema standard, which is currently draft 7.
# Common Definitions
You might have noticed above that we have a section called "definitions". This is sort of a special keyword, which is used to share common types. You can think of it like a dictionary of type definitions, to be used anywhere.
The first thing to understand about JSON Schema is that it's a "recursive" type of language. You're meant to embed types within each other, nesting upwards. So when we specify an object like:
type: object
additionalProperties: false
required: [server]
properties:
server:
description: Properties of our HTTP server
type: object
# ... other properties omitted
We're telling App Config that:
- The root config value should be an object
- It has an required property, called "server"
- That property's value should be an object
Which in effect, means our config file should look like this:
{
server: {
port: 3000,
},
}
So coming back to "definitions", this enables us to re-use those recursive types. Our use case is really simple (just a single property without nesting), but this is a powerful tool. Let's look at a more complicated example:
.app-config.schema.yaml
type: object
additionalProperties: false
required:
- database
- fallbackDatabase
- thirdPartyAPI
- adminUser
properties:
database:
$ref: '#/definitions/Postgres'
fallbackDatabase:
$ref: '#/definitions/Postgres'
thirdPartyAPI:
type: object
additionalProperties: false
required: [hostname, apiKey]
properties:
hostname:
$ref: '#/definitions/Hostname'
apiKey:
type: string
secret: true
adminUser:
$ref: '#/definitions/AdminUser'
definitions:
Postgres:
type: object
additionalProperties: false
required:
- hostname
- port
- database
- username
- password
properties:
hostname:
$ref: '#/definitions/Hostname'
port:
$ref: '#/definitions/IpPort'
database:
type: string
username:
type: string
password:
type: string
secret: true
AdminUser:
type: object
additionalProperties: false
required: [email, password]
properties:
email:
type: string
format: email
password:
type: string
secret: true
Hostname:
type: string
format: hostname
IpPort:
type: integer
minimum: 0
maximum: 65535
There's a lot going on here. We haven't really explained the $ref
part yet though.
# Type References
Take a look at the .database
property - we see a $ref
key.
This is a JSON Schema keyword that allows re-use of schema types.
It uses JSON Pointer (opens new window) syntax.
App Config supports in-file $ref
properties, as well as cross-file references.
An example:
server:
type: object
additionalProperties: false
required: [port]
properties:
port:
$ref: '../all-schemas.yml#/definitions/IpPort'
By specifying a filepath before #
, we're asking App Config to pull in another file so that we can reference a property within it.
In large monorepos with shared types, this is very handy.
References to arbitrary URLs are not supported yet (opens new window).
# Secret Properties
In the example above, we added secret: true
to a few properties like passwords.
This isn't a standard JSON Schema property.
App Config adds a simple extension to JSON Schema that's used for secret values.
As you've seen, properties are marked with a property secret
like so:
Database:
type: object
additionalProperties: false
required: [hostname, port, username, password]
properties:
hostname: { type: string }
port: { type: integer }
username: { type: string }
password: { type: string, secret: true }
This property tells App Config that it should never see password
as non-secret.
App Config will throw validation errors if it does notice this guarantee violated.
This should prevent you from accidentally committing plaintext secrets.
The rules for this are:
- Values read from the
APP_CONFIG
variable are treated as secret - Values that are read in secret files are treated as secret
- Values that are decrypted are treated as secret
- Values that are read from non-secret files (ie.
.app-config.toml
) are not treated as secret
It might seem like most values are secret, but in the majority of applications, your config will mostly live in .app-config.{ext}
files (non-secret).
# Avoiding Validation
Sometimes, you want to prototype without building out a schema. That's okay! We provide a couple ways to load configuration without validation.
- The CLI has a
--noSchema
option for most subcommands. - The Node.js API has a
loadUnvalidatedConfig
function.loadConfig
will always validate.
It's worth noting that JSON Schema can be fairly liberal if you need it to be.
To allow essentially any configuration values, just use { "type": "object" }
as the schema.
# Schemas for Type Generation
We won't dive into it here, but you should know that App Config can generate types. TypeScript is the only officially supported language at the moment.
To get the best experience with this system, you'll want to constrain as much as possible.
Without additionalProperties
, for example, the TypeScript types will be far too liberal to provide safety.
The type generation will make its best attempt to keep metadata as well, like description
(this is treated as the doc comment for properties).
You can play around with the quicktype playground (opens new window) to see what works best for you.