Radiator Server Documentation — v10.34.2

jsonfile

The JSON file backend reads a JSON document into memory and answers backend queries from it. Use it to look up user credentials, attributes, and other data. The backend supports standard JSON and JSON5 for more flexible syntax, including comments, trailing commas and unquoted keys. Both files and inline content accept these formats regardless of the file suffix.

A jsonfile query uses a JSONPath expression to select values from the document. Each selected value is one result. The backend action that calls the query processes the results in an @once or @each pipeline.

Example

The users.json file stores user entries by identity:

{
  "users": {
    "alice": {
      "password": "{argon2}$argon2id$v=19$m=19456,t=2,p=1$56MJ6kkHsbicXkvq6+r5dA$zY5kHLjEfJET8VT7hFV+uHcxgTE8w66Z4dYwwbZtdxw",
      "groups": ["admin", "user"]
    },
    "bob": {
      "password": "{argon2}$argon2id$v=19$m=19456,t=2,p=1$56MJ6kkHsbicXkvq6+r5dA$zY5kHLjEfJET8VT7hFV+uHcxgTE8w66Z4dYwwbZtdxw",
      "groups": ["user"]
    }
  }
}

The backend defines a query that selects the entry of the requesting user. The @once pipeline copies the password and groups from the entry to the user namespace, and pap verifies the password:

backends {
    jsonfile "USERS" {
        filename "users.json";

        query "FIND_USER" {
            jsonpath "$.users[$aaa.identity]";
        }
    }
}

aaa {
    policy "RADIUS" {
        conditions all {
            aaa.protocol == protocol.RADIUS;
        }

        handler "AUTHENTICATION" {
            @authentication {
                backend {
                    name "USERS";
                    query "FIND_USER";
                    @once {
                        user.password = item | jsonpath("$.password");
                        user.group = item | jsonpath("$.groups[*]");
                    }
                }
                pap;
            }
        }
    }
}

If no entry matches, or more than one entry matches, the backend action rejects and the @once pipeline does not run. See Rejecting.

Passwords in the JSON file can use secure hashing algorithms like Argon2, shown above with the {argon2} prefix. See the Password Hashing article for supported algorithms and how to generate password hashes.

Backend options

filename

Path to the JSON file. Radiator reads the file into memory when the backend starts. JSON and JSON5 syntax are supported regardless of the file suffix. A backend must have either filename or content, but not both.

content

The JSON document as an inline string. Radiator parses inline data as JSON5, so comments and trailing commas are supported. Use content to define the document in the configuration:

jsonfile "USERS" {
    content """
        {
            // Comments are allowed in JSON5
            "users": {
                "alice": { "groups": ["admin", "user"] },
            }, // Trailing commas are allowed
        }
    """;

    query "FIND_USER" {
        jsonpath "$.users[$aaa.identity]";
    }
}

monitor

Reloads the file after it changes. The default is true. See Automatic monitoring of changes.

statistics

Configures local statistics history for the backend. See Component statistics.

query

Defines a named query. A backend must have at least one query. The backend action selects the query with its query parameter.

A query can contain a jsonpath statement and a mapping block. Both are optional.

Selecting results with jsonpath

The jsonpath statement defines the query results:

query "QUERY_NAME" {
    jsonpath "<jsonpath_expression>";
}

Radiator evaluates the JSONPath expression against the parsed document. Each matched value is one result, in document order. See JSONPath for query syntax, context references, and migration from interpolated queries.

The result pipeline has access to two variables:

  • item is the current result.
  • doc is the full parsed JSON document.

Read fields from these values with the jsonpath filter. The variables are available only inside the query mappings and the result pipeline.

Processing a single result with @once

Use @once when the query must match exactly one value, such as a user entry. Radiator runs the pipeline once, with the matched value as item. If the jsonpath expression matches zero values or more than one value, the backend action rejects without running the pipeline.

backend {
    name "USERS";
    query "FIND_USER";
    @once {
        user.password = item | jsonpath("$.password");
        vars.full_userdata = item;
    }
}

Processing every result with @each

Use @each to run a pipeline for every matched value. In this example, each user entry has a group field:

{
  "users": [
    { "username": "alice", "group": "staff" },
    { "username": "bob", "group": "staff" },
    { "username": "carol", "group": "admin" }
  ]
}

The query uses a backend argument to select the users of one group, and the @each pipeline collects their user names:

jsonfile "USERS" {
    filename "users.json";

    query "FIND_GROUP" {
        jsonpath "$.users[?(@.group == $args.group)]";
    }
}
backend {
    name "USERS";
    query "FIND_GROUP";
    args {
        group = "staff";
    }
    @each {
        vars.staff_users += item | jsonpath("$.username");
    }
}

Radiator runs the pipeline once for each result, in document order. Use continue; to skip to the next result and break; to stop processing the remaining results. See @each.

If the jsonpath expression matches zero values, the backend action rejects without running the pipeline.

Queries without jsonpath

A query without a jsonpath statement has one result: the parsed document. @each and @once run once, with doc as the only variable. The query does not need any statements:

query "DOCUMENT" {
}
backend {
    name "USERS";
    query "DOCUMENT";
    @once {
        vars.all_users = doc | jsonpath("$.users[*].username");
    }
}

Mapping

The mapping block preprocesses each result before the @each or @once pipeline runs. Radiator runs the mappings for each result with the same doc and item variables, and the pipeline then uses the pre-mapped values.

Map the fields of each result to vars.* variables in the query, and read these variables in the pipeline. The pipeline then does not need to know the format of the JSON file or use the jsonpath filter. If the format of the file changes, update only the query mappings.

This query maps the fields of each user entry in the @each example to vars.username and vars.group:

query "FIND_GROUP" {
    jsonpath "$.users[?(@.group == $args.group)]";
    mapping {
        vars.username = item | jsonpath("$.username");
        vars.group = item | jsonpath("$.group");
    }
}

Each @each run sees the values mapped from the current result, so the pipeline uses vars.username instead of item:

backend {
    name "USERS";
    query "FIND_GROUP";
    args {
        group = "staff";
    }
    @each {
        vars.staff_users += vars.username;
    }
}

The mappings also run when the backend action has no @each or @once pipeline. Variables set by the mappings stay set after each result. With =, each result overwrites the value, so after the backend action the variable holds the value of the last result. With +=, the values of all results accumulate.

Mapping without a result pipeline

The ipmap backend uses a jsonpath statement to load its entries from a JSON file. The mappings run for each result, and each run commits one network prefix to the map:

jsonfile "CLIENTS" {
    filename "clients.json";

    query "POPULATE_IPMAP" {
        jsonpath "$.clients[*]";
        mapping {
            ipmap.network = item | jsonpath("$.ip");
            ipmap.value = item | jsonpath("$.name");
        }
    }
}
{
  "clients": [
    { "ip": "192.0.2.0/24", "name": "internal-network" },
    { "ip": "198.51.100.0/24", "name": "guest-network" }
  ]
}

Rejecting

The backend action rejects in these cases:

  • With a jsonpath statement: The jsonpath expression matches zero values, including when a context reference is missing. With @once, the backend action also rejects when the expression matches more than one value. The values that the mappings produce do not affect the result.
  • Without a jsonpath statement: All mapping expressions return none. A query with an empty mapping block does not reject.

When the backend action rejects, Radiator does not run the @each or @once pipeline. Radiator sets aaa.reason to a message that includes the backend name and query name.

Automatic monitoring of changes

When enabling monitor true Radiator checks for changes in the file and reloads it. This happens usually within one second of last change. If the file changes to invalid JSON, the backend will keep serving the last valid content (until a restart).

The JSON file is read into memory resulting in high performance lookups.

Reloading reads and parses the entire file. The previously loaded document remains available while the new document is loaded. Allow memory for both documents and the temporary allocations needed during loading. The file format and dataset size affect reload time and temporary memory usage.

Radiator monitors the whole directory where the file is located to detect addition of a file to be able detect when the file is created and not just modified. Writing logs or editing files in the directory being watched will have a performance impact. It is recommended to separate these activities to different directories and to use mv or move to move the final file to production directory.

Do not modify/replace the file while it is being read. Prefer batching (aggregated) updates at regular intervals over frequent small changes for more consistent and reliable performance.