Modeling a file system with JSON Schema

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:

data
1
{
2
"/": {
3
"storage": {
4
"type": "disk",
5
"device": "/dev/sda1"
6
},
7
"fstype": "btrfs",
8
"readonly": true
9
},
10
"/var": {
11
"storage": {
12
"type": "disk",
13
"label": "8f3ba6f4-5c70-46ec-83af-0d5434953e5f"
14
},
15
"fstype": "ext4",
16
"options": [ "nosuid" ]
17
},
18
"/tmp": {
19
"storage": {
20
"type": "tmpfs",
21
"sizeInMB": 64
22
}
23
},
24
"/var/www": {
25
"storage": {
26
"type": "nfs",
27
"server": "my.nfs.server",
28
"remotePath": "/exports/mypath"
29
}
30
}
31
}

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.

schema
1
{
2
"$id": "https://example.com/fstab",
3
"$schema": "https://json-schema.org/draft/2020-12/schema",
4
"type": "object",
5
"required": [ "/" ],
6
"properties": {
7
"/": {}
8
},
9
"patternProperties": {
10
"^(/[^/]+)+$": {}
11
},
12
"additionalProperties": false
13
}

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.
schema
1
{
2
"$id": "https://example.com/entry-schema",
3
"$schema": "https://json-schema.org/draft/2020-12/schema",
4
"description": "JSON Schema for an fstab entry",
5
"type": "object",
6
"required": [ "storage" ],
7
"properties": {
8
"storage": {
9
"type": "object",
10
"oneOf": [
11
{ "$ref": "#/$defs/diskDevice" },
12
{ "$ref": "#/$defs/diskUUID" },
13
{ "$ref": "#/$defs/nfs" },
14
{ "$ref": "#/$defs/tmpfs" }
15
]
16
}
17
},
18
"$defs": {
19
"diskDevice": {},
20
"diskUUID": {},
21
"nfs": {},
22
"tmpfs": {}
23
}
24
}

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:

schema
1
{
2
"$id": "https://example.com/entry-schema",
3
"$schema": "https://json-schema.org/draft/2020-12/schema",
4
"description": "JSON Schema for an fstab entry",
5
"type": "object",
6
"required": [ "storage" ],
7
"properties": {
8
"storage": {
9
"type": "object",
10
"oneOf": [
11
{ "$ref": "#/$defs/diskDevice" },
12
{ "$ref": "#/$defs/diskUUID" },
13
{ "$ref": "#/$defs/nfs" },
14
{ "$ref": "#/$defs/tmpfs" }
15
]
16
},
17
"fstype": {
18
"enum": [ "ext3", "ext4", "btrfs" ]
19
},
20
"options": {
21
"type": "array",
22
"minItems": 1,
23
"items": {
24
"type": "string"
25
},
26
"uniqueItems": true
27
},
28
"readonly": {
29
"type": "boolean"
30
}
31
},
32
"$defs": {
33
"diskDevice": {},
34
"diskUUID": {},
35
"nfs": {},
36
"tmpfs": {}
37
}
38
}

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.
data
1
{
2
"diskDevice": {
3
"properties": {
4
"type": {
5
"enum": [ "disk" ]
6
},
7
"device": {
8
"type": "string",
9
"pattern": "^/dev/[^/]+(/[^/]+)*$"
10
}
11
},
12
"required": [ "type", "device" ],
13
"additionalProperties": false
14
}
15
}

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.

data
1
{
2
"diskUUID": {
3
"properties": {
4
"type": {
5
"enum": [ "disk" ]
6
},
7
"label": {
8
"type": "string",
9
"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}$"
10
}
11
},
12
"required": [ "type", "label" ],
13
"additionalProperties": false
14
}
15
}

The nfs definition

We find another new keyword:

  • The format annotation and assertion keyword.
data
1
{
2
"nfs": {
3
"properties": {
4
"type": { "enum": [ "nfs" ] },
5
"remotePath": {
6
"type": "string",
7
"pattern": "^(/[^/]+)+$"
8
},
9
"server": {
10
"type": "string",
11
"oneOf": [
12
{ "format": "hostname" },
13
{ "format": "ipv4" },
14
{ "format": "ipv6" }
15
]
16
}
17
},
18
"required": [ "type", "server", "remotePath" ],
19
"additionalProperties": false
20
}
21
}

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.
data
1
{
2
"tmpfs": {
3
"properties": {
4
"type": { "enum": [ "tmpfs" ] },
5
"sizeInMB": {
6
"type": "integer",
7
"minimum": 16,
8
"maximum": 512
9
}
10
},
11
"required": [ "type", "sizeInMB" ],
12
"additionalProperties": false
13
}
14
}

The full entry schema

The resulting schema is quite large:

schema
1
{
2
"$id": "https://example.com/entry-schema",
3
"$schema": "https://json-schema.org/draft/2020-12/schema",
4
"description": "JSON Schema for an fstab entry",
5
"type": "object",
6
"required": [ "storage" ],
7
"properties": {
8
"storage": {
9
"type": "object",
10
"oneOf": [
11
{ "$ref": "#/$defs/diskDevice" },
12
{ "$ref": "#/$defs/diskUUID" },
13
{ "$ref": "#/$defs/nfs" },
14
{ "$ref": "#/$defs/tmpfs" }
15
]
16
},
17
"fstype": {
18
"enum": [ "ext3", "ext4", "btrfs" ]
19
},
20
"options": {
21
"type": "array",
22
"minItems": 1,
23
"items": {
24
"type": "string"
25
},
26
"uniqueItems": true
27
},
28
"readonly": {
29
"type": "boolean"
30
}
31
},
32
"$defs": {
33
"diskDevice": {
34
"properties": {
35
"type": {
36
"enum": [ "disk" ]
37
},
38
"device": {
39
"type": "string",
40
"pattern": "^/dev/[^/]+(/[^/]+)*$"
41
}
42
},
43
"required": [ "type", "device" ],
44
"additionalProperties": false
45
},
46
"diskUUID": {
47
"properties": {
48
"type": {
49
"enum": [ "disk" ]
50
},
51
"label": {
52
"type": "string",
53
"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}$"
54
}
55
},
56
"required": [ "type", "label" ],
57
"additionalProperties": false
58
},
59
"nfs": {
60
"properties": {
61
"type": { "enum": [ "nfs" ] },
62
"remotePath": {
63
"type": "string",
64
"pattern": "^(/[^/]+)+$"
65
},
66
"server": {
67
"type": "string",
68
"oneOf": [
69
{ "format": "hostname" },
70
{ "format": "ipv4" },
71
{ "format": "ipv6" }
72
]
73
}
74
},
75
"required": [ "type", "server", "remotePath" ],
76
"additionalProperties": false
77
},
78
"tmpfs": {
79
"properties": {
80
"type": { "enum": [ "tmpfs" ] },
81
"sizeInMB": {
82
"type": "integer",
83
"minimum": 16,
84
"maximum": 512
85
}
86
},
87
"required": [ "type", "sizeInMB" ],
88
"additionalProperties": false
89
}
90
}
91
}

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.
schema
1
{
2
"$id": "https://example.com/fstab",
3
"$schema": "https://json-schema.org/draft/2020-12/schema",
4
"type": "object",
5
"required": [ "/" ],
6
"properties": {
7
"/": { "$ref": "https://example.com/entry-schema" }
8
},
9
"patternProperties": {
10
"^(/[^/]+)+$": { "$ref": "https://example.com/entry-schema" }
11
},
12
"additionalProperties": false
13
}