Introduction #

Not all constraints to an fstab file can be modeled using JSON Schema alone; however, it can represent a good number of them and the exercise is useful to demonstrate how constraints work. The examples provided are illustrative of the JSON Schema concepts rather than a real, working schema for an fstab file.

This example shows a possible JSON Schema representation of file system mount points as represented in an /etc/fstab file.

An entry in an fstab file can have many different forms; Here is an example:

{
  "/": {
    "storage": {
      "type": "disk",
      "device": "/dev/sda1"
    },
    "fstype": "btrfs",
    "readonly": true
  },
  "/var": {
    "storage": {
      "type": "disk",
      "label": "8f3ba6f4-5c70-46ec-83af-0d5434953e5f"
    },
    "fstype": "ext4",
    "options": [ "nosuid" ]
  },
  "/tmp": {
    "storage": {
      "type": "tmpfs",
      "sizeInMB": 64
    }
  },
  "/var/www": {
    "storage": {
      "type": "nfs",
      "server": "my.nfs.server",
      "remotePath": "/exports/mypath"
    }
  }
}

Creating the fstab schema #

We will start with a base JSON Schema expressing the following constraints:

  • the list of entries is a JSON object;
  • the member names (or property names) of this object must all be valid, absolute paths;
  • there must be an entry for the root filesystem (ie, /).

Building out our JSON Schema from top to bottom:

  • The $id keyword.
  • The $schema keyword.
  • The type validation keyword.
  • The required validation keyword.
  • The properties validation keyword.
    • The / key is empty now; We will fill it out later.
  • The patternProperties validation keyword.
    • This matches other property names via a regular expression. Note: it does not match /.
    • The ^(/[^/]+)+$ key is empty now; We will fill it out later.
  • The additionalProperties validation keyword.
    • The value here is false to constrain object properties to be either / or to match the regular expression.

You will notice that the regular expression is explicitly anchored (with ^ and $): in JSON Schema, regular expressions (in patternProperties and in pattern) are not anchored by default.

{
  "$id": "https://example.com/fstab",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": [ "/" ],
  "properties": {
    "/": {}
  },
  "patternProperties": {
    "^(/[^/]+)+$": {}
  },
  "additionalProperties": false,
}

Starting the entry schema #

We will start with an outline of the JSON schema which adds new concepts to what we’ve already demonstrated.

We saw these keywords in the prior exercise: $id, $schema, type, required and properties.

To this we add:

  • The description annotation keyword.
  • The oneOf keyword.
  • The $ref keyword.
    • In this case, all references used are local to the schema using a relative fragment URI (#/...).
  • The $defs keyword.
    • Including several key names which we will define later.
{
  "$id": "https://example.com/entry-schema",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "description": "JSON Schema for an fstab entry",
  "type": "object",
  "required": [ "storage" ],
  "properties": {
    "storage": {
      "type": "object",
      "oneOf": [
        { "$ref": "#/$defs/diskDevice" },
        { "$ref": "#/$defs/diskUUID" },
        { "$ref": "#/$defs/nfs" },
        { "$ref": "#/$defs/tmpfs" }
      ]
    }
  },
  "$defs": {
    "diskDevice": {},
    "diskUUID": {},
    "nfs": {},
    "tmpfs": {}
  }
}

Constraining an entry #

Let’s now extend this skeleton to add constraints to some of the properties.

  • Our fstype key uses the enum validation keyword.
  • Our options key uses the following:
    • The type validation keyword (see above).
    • The minItems validation keyword.
    • The items validation keyword.
    • The uniqueItems validation keyword.
    • Together these say: options must be an array, and the items therein must be strings, there must be at least one item, and all items should be unique.
  • We have a readonly key.

With these added constraints, the schema now looks like this:

{
  "$id": "https://example.com/entry-schema",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "description": "JSON Schema for an fstab entry",
  "type": "object",
  "required": [ "storage" ],
  "properties": {
    "storage": {
      "type": "object",
      "oneOf": [
        { "$ref": "#/$defs/diskDevice" },
        { "$ref": "#/$defs/diskUUID" },
        { "$ref": "#/$defs/nfs" },
        { "$ref": "#/$defs/tmpfs" }
      ]
    },
    "fstype": {
      "enum": [ "ext3", "ext4", "btrfs" ]
    },
    "options": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "string"
      },
      "uniqueItems": true
    },
    "readonly": {
      "type": "boolean"
    }
  },
  "$defs": {
    "diskDevice": {},
    "diskUUID": {},
    "nfs": {},
    "tmpfs": {}
  }
}

The diskDevice definition #

One new keyword is introduced here:

  • The pattern validation keyword notes the device key must be an absolute path starting with /dev.
{
  "diskDevice": {
    "properties": {
      "type": {
        "enum": [ "disk" ]
      },
      "device": {
        "type": "string",
        "pattern": "^/dev/[^/]+(/[^/]+)*$"
      }
    },
    "required": [ "type", "device" ],
    "additionalProperties": false
  }
}

The diskUUID definition #

No new keywords are introduced here.

We do have a new key: label and the pattern validation keyword states it must be a valid UUID.

{
  "diskUUID": {
    "properties": {
      "type": {
        "enum": [ "disk" ]
      },
      "label": {
        "type": "string",
        "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
      }
    },
    "required": [ "type", "label" ],
    "additionalProperties": false
  }
}

The nfs definition #

We find another new keyword:

  • The format annotation and assertion keyword.
{
  "nfs": {
    "properties": {
      "type": { "enum": [ "nfs" ] },
      "remotePath": {
        "type": "string",
        "pattern": "^(/[^/]+)+$"
      },
      "server": {
        "type": "string",
        "oneOf": [
          { "format": "hostname" },
          { "format": "ipv4" },
          { "format": "ipv6" }
        ]
      }
    },
    "required": [ "type", "server", "remotePath" ],
    "additionalProperties": false
  }
}

The tmpfs definition #

Our last definition introduces two new keywords:

  • The minimum validation keyword.
  • The maximum validation keyword.
  • Together these require the size be between 16 and 512, inclusive.
{
  "tmpfs": {
    "properties": {
      "type": { "enum": [ "tmpfs" ] },
      "sizeInMB": {
        "type": "integer",
        "minimum": 16,
        "maximum": 512
      }
    },
    "required": [ "type", "sizeInMB" ],
    "additionalProperties": false
  }
}

The full entry schema #

The resulting schema is quite large:

{
  "$id": "https://example.com/entry-schema",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "description": "JSON Schema for an fstab entry",
  "type": "object",
  "required": [ "storage" ],
  "properties": {
    "storage": {
      "type": "object",
      "oneOf": [
        { "$ref": "#/$defs/diskDevice" },
        { "$ref": "#/$defs/diskUUID" },
        { "$ref": "#/$defs/nfs" },
        { "$ref": "#/$defs/tmpfs" }
      ]
    },
    "fstype": {
      "enum": [ "ext3", "ext4", "btrfs" ]
    },
    "options": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "string"
      },
      "uniqueItems": true
    },
    "readonly": {
      "type": "boolean"
    }
  },
  "$defs": {
    "diskDevice": {
      "properties": {
        "type": {
          "enum": [ "disk" ]
        },
        "device": {
          "type": "string",
          "pattern": "^/dev/[^/]+(/[^/]+)*$"
        }
      },
      "required": [ "type", "device" ],
      "additionalProperties": false
    },
    "diskUUID": {
      "properties": {
        "type": {
          "enum": [ "disk" ]
        },
        "label": {
          "type": "string",
          "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
        }
      },
      "required": [ "type", "label" ],
      "additionalProperties": false
    },
    "nfs": {
      "properties": {
        "type": { "enum": [ "nfs" ] },
        "remotePath": {
          "type": "string",
          "pattern": "^(/[^/]+)+$"
        },
        "server": {
          "type": "string",
          "oneOf": [
            { "format": "hostname" },
            { "format": "ipv4" },
            { "format": "ipv6" }
          ]
        }
      },
      "required": [ "type", "server", "remotePath" ],
      "additionalProperties": false
    },
    "tmpfs": {
      "properties": {
        "type": { "enum": [ "tmpfs" ] },
        "sizeInMB": {
          "type": "integer",
          "minimum": 16,
          "maximum": 512
        }
      },
      "required": [ "type", "sizeInMB" ],
      "additionalProperties": false
    }
  }
}

Referencing the entry schema in the fstab schema #

Coming full circle we use the $ref keyword to add our entry schema into the keys left empty at the start of the exercise:

  • The / key.
  • The ^(/[^/]+)+$ key.
{
  "$id": "https://example.com/fstab",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": [ "/" ],
  "properties": {
    "/": { "$ref": "https://example.com/entry-schema" }
  },
  "patternProperties": {
    "^(/[^/]+)+$":  { "$ref": "https://example.com/entry-schema" }
  },
  "additionalProperties": false
}