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.
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.
The file
Section titled “The file”version: 1name: petshop-idsdescription: 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.
Where a value goes
Section titled “Where a value goes”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.
Naming an operation
Section titled “Naming an operation”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.
Naming a place in the request
Section titled “Naming a place in the request”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.
The values
Section titled “The values”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: 1name: bodieskeyedBy: operationAndParameter
values: addOwner: body: - firstName: George lastName: Franklin city: MadisonRESTest 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.
A body piece by piece, or a body whole
Section titled “A body piece by piece, or a body whole”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.
Which lists get used for what
Section titled “Which lists get used for what”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: dictionariesbefore the group: sources outside a group are asked in order, and the first answer is taken (the campaign file).
observed is not one of these
Section titled “observed is not one of these”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.
What a run says about your file
Section titled “What a run says about your file”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 nulland 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.
Writing one of these from a specification
Section titled “Writing one of these from a specification”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:
- Name every operation by its method and path unless you are certain of its
operationId. Both are accepted, and this one needs no checking. - Write
body.in front of anything inside the request body. A barecitymeans a parameter calledcity, and if the operation has no such parameter the entry does nothing. - Skip parameters the document gives an
enumfor. The document has already said what those values are, and nothing in your file will be sent instead. - Prefer a list per place over a whole body, and keep the whole body for objects whose fields depend on each other.
- Repeat a list under every operation that takes it. A value good for
ownerIdingetOwneris written again underupdateOwneranddeleteOwner: 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.