Skip to content

The dictionary format

A dictionary is a list of values worth sending, in a YAML file. RESTest reads them, adds them to the sources it asks when it needs a value, and uses them alongside everything the specification itself says.

This is a published format. YAML, with nothing to install and no tooling to run, because the point of a dictionary is that a person can write one in an afternoon — or a script can produce one — and commit it next to the specification it belongs to, which is written in YAML too. JSON is a subset of YAML, so a file written that way is read the same way without anybody being told. A JSON Schema of the format lets an editor, or a language model writing one, get its shape right before a run; a file never needs it to be read.

Terminal window
restest run api.yaml --url https://api.example --dictionary ids.yaml --dictionary ./dictionaries/

--dictionary takes a file or a directory, and may be repeated. A directory contributes every .yaml, .yml and .json file in it, read in name order. RESTest always holds its own list of values to push at an API with on top of whatever you give it, for a strategy of kind fuzzing and for the changes a strategy of kind mutation makes.

A file that cannot be read costs the values in it and is reported — as the run starts, again at the end of its summary, and in report.json (see What a run says about your file). It never ends the run.

version: 1
name: petshop-ids
description: Identifiers that exist in the staging database.
keyedBy: operationAndParameter
values:
getOwner:
ownerId: [1, 2, 3]
"GET /pets/{petId}":
petId: [7]
Field
version required The version of this format, not of your file. Today it is 1. A file written in a version RESTest does not know is refused by name rather than half-read
name required What this dictionary is called. A plan names the dictionaries each kind of request draws on, so the name is how a list gets used, and it is what a report prints beside a value that came from it
description optional For whoever reads the file next: a piece of text, or nothing at all
keyedBy required Always operationAndParameter: each list sits under its operation, and under the place in that operation's request it goes
values required The values themselves, under their keys

Anything else in the file is refused by name, because a member nobody recognises is usually a misspelling. The same goes for a key written twice, which YAML allows and which would otherwise leave the first list quietly replaced.

One dictionary per file, under the one name a plan points at it by. Keep several by keeping several files.

fuzzing is not a name you can give a list. It is the name of the list of values RESTest carries to push at an API with, and a file of yours called that is refused. The values a run pushes at an API with are RESTest's own.

A dictionary does not say what the API will make of its values, and could not. A value can be perfectly good and the request still refused for a rule about some other parameter; and a value that looks outrageous — an empty word, a zero, a list with nothing in it — is one a great many APIs accept quite happily. What a list is for is decided by the plan that names it, not by the file.

Every list in a file sits under an operation, and under the place in that operation's request its values go. Writing one means going through the API's document operation by operation, which is what makes the file exact: it says where everything it holds is sent, and nowhere else.

A value meant for several places is written under each of them. A surname good for the search's lastName and for the body that creates an owner goes under both:

values:
listOwners:
lastName: [Franklin, Davis]
addOwner:
body.lastName: [Franklin, Davis]

YAML's own anchors save writing a list twice: lastName: &surnames [Franklin, Davis] names it, and body.lastName: *surnames repeats it.

Nothing here needs quoting to be safe. YAML's own older rules turn a startling number of ordinary words into something else — no, off and n into false, yes, on and y into true, an unquoted date into a date — so a list of country codes holding NO would send false and never say so. RESTest recognises only what JSON itself has: true, false, null, a whole number and a number. Everything else is a piece of text, which is what somebody writing a list of values meant.

Numbers keep every digit the file wrote, rather than the nearest value sixty-four bits can hold, so a deliberately enormous one arrives as it was written.

An operation answers to two names, and either will do:

  • its operationId, when the document declares one — getPetById;
  • its method and its path, always — GET /pets/{petId}: the method in capitals, one space, and the path exactly as the document writes it, braces and all.

The second is there so that a file can be written from the specification with no reasoning and no exceptions. If you would rather not check whether a document declares identifiers, use the method and the path everywhere and you will always be right. RESTest turns them into whichever name it prints in its own output, so the two never come apart.

Unquoted is fine. YAML is happy with GET /pets/{petId}: as a key.

An operation with nothing under it is an operation with nothing to fill in, and so is values: with nothing under it. A comment on its own is fine:

values:
GET /pets:
# nothing to fill: every place is settled by the document
getOwner:
ownerId: [1, 2, 3]

A place with nothing under it is not: ownerId: alone is refused, because it could mean no values or the single value null. Write [] for the first and [null] for the second.

Inside an operation, a key names one place a value goes:

Key Means
ownerId the parameter of that name, wherever the document puts it — path, query string, header, cookie
tags[] every element of the parameter tags, where the document declares it a list
filter.city the property city of the parameter filter, where the document declares it an object
body the whole request body, as one value
body.city the property city at the top of the body
body.owner.email the property email of the object under owner
body.tags[].label the label of every element of the list tags
body[] every element, when the body is itself a list

A parameter has pieces just as a body does, and they are written the same way. Most of the pieces in most APIs are in the body, which is why the examples lean that way.

[] stands for every element because there is no one element a value could be meant for: a list of values is drawn from each time an element is built.

A name declared in two places — a petId in the path and a petId in the query string — gets the entry in both, and there is no way to say which you meant: the key is the name, and both parameters have it. Where they mean different things, write values that suit either, or leave the entry out and let the document answer.

A value is anything JSON can carry: a word, a number, true, null, a list, or a whole object. That last one is what makes a dictionary useful for request bodies, where the thing worth keeping is the object rather than any field of it.

version: 1
name: bodies
keyedBy: operationAndParameter
values:
addOwner:
body:
- firstName: George
lastName: Franklin
city: Madison

RESTest picks among the values that apply at random rather than always the first, so a run works through the whole list. A value that could not actually be put in the request — one that would leave a gap in a path empty, or carry a line break into a header — is skipped for that place and used elsewhere.

Both work, and they are for different things.

values:
addOwner:
body.city: [Madison, Seville] # a list per piece
body.telephone: ["6085551023"]
addVisit:
body: # or the whole thing at once
- {date: "2026-09-18", description: rabies shot}

A list per piece is the one to reach for. RESTest still assembles the body, so what the document says still applies around your values — the formats it declares, the lists of allowed values, which optional properties to include this time — and the body varies from request to request.

One thing to know before writing a list per piece: where the document offers a whole sample body of its own, that sample is what gets sent, and nothing inside it is ever asked for, so entries for its pieces do nothing. The run says so when it reads your file. It is commoner than it sounds — all twelve of kafka-rest-proxy's bodies are written out in full in its document.

A whole body is for when the object only makes sense as a whole: when one field constrains another, when the body is not an object at all (a bare list, a single word), or when you have a payload you know the API accepts and want it sent exactly as written. What you give up is variation: that object is sent as it stands, every time it is drawn.

Where a file gives both, the whole body wins and the pieces of it are never used — the point of writing a body whole is that its fields agree with each other, and quietly replacing one of them would undo that. RESTest says so when it reads the file.

A run does not build every request the same way. Most of its time goes on requests meant to work; some of it goes on pushing at the API with values nobody sensible would send, which is how an API gets asked what it does with the unexpected. Those are kept apart: an API stops reading at the first thing it does not like, so one odd value among good ones teaches nothing that a request of all odd values does not.

Which lists feed which kind of request is decided by the plan, and the plan is a file you can write. The one RESTest carries asks for every list you handed over, in the order you handed them over, when building requests meant to work; its strategy of kind fuzzing pushes at the API with RESTest's own list, written for kinds of value rather than for any API's places, which comes with that kind and is not named. In a plan of your own, a list can be named by its name, one list can feed two kinds of request, or two lists one kind.

One more thing reads RESTest's fuzzing list. When a request the API accepted is sent again with one value of the wrong kind in it — see the campaign file — that value is drawn from that list as well as from a few more RESTest keeps for it, and never from a list of yours, whose values belong to the places it names. Only the values the document refuses for the place they go to are sent there.

Where a parameter declares an enum, nothing in your file is used for it. That list is not advice: it is the whole set of values the API says it takes, and sending anything else would be sending a value the document has already refused. The one exception is an enumeration none of whose values could be put where that parameter goes — every one of them empty, in a path — which is read as no enumeration at all, because the alternative is an operation that can never be tested.

In the plan RESTest carries, a list is one voice among several, not a replacement. Its requests meant to work draw from a weighted group: the document's samples and defaults, values invented to fit, what the API has returned, and your lists. Every source in the group that has an answer for a place is asked, and one answer is drawn by weight. Your lists weigh 40 of 100, so where all five answer, about two requests in five carry one of your values, and more where fewer of the others have anything to say. Your lists only ever answer at the places they name. To make them the only answer wherever they have one, write a plan that asks source: dictionaries before the group: sources outside a group are asked in order, and the first answer is taken (the campaign file).

A run can also draw on what the API itself has sent back: a value kept under the name it came back under, and a whole thing under the name of its shape. It is not a file, though, and there is nothing here to write for it. A plan asks for it with source: observed, the way it asks for the document's own samples, and the campaign format says what it does and what it costs. A file of yours called observed is an ordinary list with an ordinary name, and a plan names it the ordinary way.

Everything a document can settle is settled when the file is read, before a single request is sent, and said in one line per file:

restest: ids.yaml: 5 of its 8 entries will never be used: 1 for no such operation in this API
(ownerId in getOwnerRenamedSince), 1 for a piece of a body that is supplied whole, which
is sent instead (body.city in addOwner), 3 for no such parameter or piece of a body in
that operation (postcode in addOwner, firstName in addOwner, body.nonsense in addVisit)

Eight things earn a mention:

What Usually means
no such operation in this API the file has fallen behind the document — or the operation gained an operationId since
an operation the document could not be read for the document describes it, but RESTest could not read it, and names it among the operations the run could not test
no such parameter or piece of a body in that operation a misspelling, or a property written as city where it should be body.city
a place whose whole list of values the document declares an enum, which nothing overrides, wherever it is
a property the document says the API only ever sends back readOnly, which is never ours to send
a piece of a body that is supplied whole some list gives that operation a whole body, and that is what gets sent
a piece of a body the document itself writes out in full the document offers a whole sample body, which is sent as the author wrote it
a body, or a piece of one, on a GET or a HEAD, which RESTest never sends the document offers a body on a GET or HEAD; RESTest cannot send one, so the operation is tested without it — or, if it insists on one, not at all

A line of its own appears when one file writes one operation under both of the names it answers to. The entries under the operationId are the ones kept.

None of it ends the run. A dictionary is advice, and a run with some of it unusable is still a run.

A file that could not be read at all — or a directory with no dictionary in it, or a path with nothing there — is said three times, because a run made without the values somebody meant it to have looks, from its results alone, exactly like a run made with them that found they did not help. Once as the run starts, once more at the very end of the summary:

1 list of values could not be read, so none of its values were sent:
ids.yaml has nothing under 'ownerId' in 'getOwner', where a list of values belongs: write [] for no values, or [null] to send null

and once in report.json, which names every list the run holds — RESTest's own included — with the file it came from, beside every one it refused:

"dictionaries": {
"read": [{"name": "fuzzing", "from": "the list of values to push with that is built into RESTest"}],
"refused": [{"from": "ids.yaml", "reason": "ids.yaml has nothing under 'ownerId' in 'getOwner', ..."}]
}

Anything that runs RESTest with a file of yours and compares the results can check that the file is under read and nothing is under refused before it believes them. Being read is not the same as being used — which lists each kind of request asks is the plan's to decide, and a plan with no strategy of kind fuzzing or mutation, for one, leaves the list RESTest carries held and unasked — but a file that was never read was certainly not used.

A file that is read and holds not a single value — every entry commented out, say — is not refused, since nothing in it is wrong, but it is said as the run starts, and so is an operation written with nothing under it that the API does not have.

A file is often produced from the OpenAPI document by a person or a program working through it operation by operation. Five rules make that reliable:

  1. Name every operation by its method and path unless you are certain of its operationId. Both are accepted, and this one needs no checking.
  2. Write body. in front of anything inside the request body. A bare city means a parameter called city, and if the operation has no such parameter the entry does nothing.
  3. Skip parameters the document gives an enum for. The document has already said what those values are, and nothing in your file will be sent instead.
  4. Prefer a list per place over a whole body, and keep the whole body for objects whose fields depend on each other.
  5. Repeat a list under every operation that takes it. A value good for ownerId in getOwner is written again under updateOwner and deleteOwner: a list only ever answers at the places it names.

Before a run, check the file against the format's JSON Schema, or have a language model held to it while it writes: that settles the file's shape, though not whether its operations and places are the API's. Then look at what the run says about the file. The check happens as the file is read, before the first request, so the first few lines of any run tell you what is wrong with it. There is no command that reads a file and stops; until there is, point a short run at a server of your own rather than at somebody else's API to see them.