backends.jsonfile
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:
itemis the current result.docis 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
jsonpathstatement: Thejsonpathexpression 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
jsonpathstatement: All mapping expressions returnnone. A query with an emptymappingblock 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.