{"title":"Design Negative API Tests from the Contract","excerpt":"A negative test that asserts only a 4xx status is satisfied by a crashed pod, a rewritten gateway response, and a rejection that already decremented stock. This guide derives one invalid request per contract rule, places each mutation on the boundary the rule actually defines, and gives every case a three-part oracle: the exact status code, the machine-readable location of the violation, and a probe proving no forbidden side effect survived the rejection.","canonicalUrl":"https://automationtester.in/blog/automation-tutorials/design-negative-api-tests-from-contract","category":{"name":"Automation Tutorials","slug":"automation-tutorials"},"tags":["api-contracts","negative-testing","openapi","json-schema","playwright","http-status-codes"],"author":{"name":"Shashank Rawlani","url":"https://shashank.rawlani.com","linkedin":"https://www.linkedin.com/in/shashankrawlani/"},"datePublished":"2026-09-13T13:30:00.000Z","dateModified":"2026-09-07T11:52:05.307Z","coverImage":{"url":"https://ik.imagekit.io/automationtester/automationtester-in/blog/covers/v3/design-negative-api-tests-from-contract.webp","alt":"Dark technical illustration reading left to right. A green request panel of stacked pills sits on the left, one of its pills half green and half orange. A line runs from that pill to a tick-marked ruler crossed by a dashed vertical line: an orange crossed-out circle sits far to the left of the dashed line, while an orange ring and a green ring sit close together on either side of it. The ruler feeds a dashed rectangular gate with a large ringed node. Two branches leave the gate: an orange branch reaching an orange panel where one pill is drawn in green with a small ring resting on it, and a green branch reaching a green panel holding three pills of identical length each ending in a dot. A dashed orange curve drops from the gate and terminates at a crossed-out circle below the second panel."},"contentHtml":"<div class=\"callout callout-info\"><strong>Quick answer:</strong> Derive one invalid request per contract rule, place it on the rule's boundary rather than far outside it, and give each case an oracle with three parts: the exact status code, the machine-readable location of the violation, and a probe proving no forbidden side effect occurred. A case that asserts only <code>status &gt;= 400</code> cannot tell a correct rejection from a crash, and a case that changes several fields at once cannot say which rule the service failed to enforce.</div>\n\n<p><code>expect(response.status()).toBeGreaterThanOrEqual(400)</code> is the most common assertion in negative API testing and one of the least informative. It passes when the service correctly rejects a malformed order. It also passes when the service crashes, when a proxy returns an HTML error page, and when the route has been deleted.</p>\n\n<p>Replace a validation layer underneath a suite built that way and it stays green through all of it: unknown enum values answering 500 where they used to answer 422, a rejected order that still decremented warehouse stock, another tenant's invoice returning 403 where it previously returned 404 and so confirming which invoice IDs exist. Those are three of the failures a negative test exists to catch, and a 4xx-or-worse assertion is satisfied by every one of them.</p>\n\n<h2 id=\"one-mutation-per-rule-on-the-boundary\">One mutation per rule, and put it on the boundary</h2>\n\n<p>A case must change exactly one thing relative to a known-valid request, and the change must sit at the edge of the rule it targets rather than somewhere comfortably beyond it. The second half is the part usually skipped. Consider an order line whose quantity must be at least one:</p>\n\n<pre class=\"language-yaml\"><code class=\"language-yaml\"># The rule under test\nquantity:\n  type: integer\n  minimum: 1</code></pre>\n\n<p>A negative case sending <code>quantity: -1</code> feels thorough and proves very little. JSON Schema 2020-12 defines <code>minimum</code> as an inclusive lower limit and <code>exclusiveMinimum</code> as a strict one, so <code>minimum: 1</code> and <code>exclusiveMinimum: 0</code> disagree about exactly one value: zero. A request carrying <code>-1</code> is rejected under either spelling, so it cannot detect the day someone edits the schema from one to the other. The case that discriminates is <code>quantity: 0</code>, paired with a positive case at <code>quantity: 1</code>.</p>\n\n<pre class=\"language-ts\"><code class=\"language-ts\">import { test, expect } from '@playwright/test';\nimport { validOrder } from './fixtures/valid-order';\n\n// Proves nothing about where the boundary is.\ntest('rejects a negative quantity', async ({ request }) =&gt; {\n  const response = await request.post('/v1/orders', {\n    data: { ...validOrder, quantity: -1 },\n  });\n  expect(response.status()).toBe(422);\n});\n\n// Pins the boundary: 0 must be rejected and 1 must be accepted.\ntest('quantity boundary sits between 0 and 1', async ({ request }) =&gt; {\n  const rejected = await request.post('/v1/orders', {\n    data: { ...validOrder, quantity: 0 },\n  });\n  expect(rejected.status()).toBe(422);\n\n  const accepted = await request.post('/v1/orders', {\n    data: { ...validOrder, quantity: 1 },\n  });\n  expect(accepted.status()).toBe(201);\n});</code></pre>\n\n<p>Type rules carry a sharper version of the same trap. JSON Schema's <code>\"integer\"</code> matches <em>any number with a zero fractional part</em>, so a case built around <code>quantity: 2.0</code> is accepted, correctly, while a reviewer reading the test name believes non-integer quantities are rejected. The value that exercises the rule is <code>2.5</code>.</p>\n\n<p><code>required</code> has a comparable edge: it is satisfied when every listed name <em>appears as a property</em>, and says nothing about the value. So <code>{ \"quantity\": null }</code> satisfies <code>required: [\"quantity\"]</code> and is rejected by <code>type</code> instead. Sending null to test a required field writes a type case and labels it a presence case. Omit the key.</p>\n\n<h2 id=\"mutations-live-in-the-description-defaults-included\">Read the mutations off the description, defaults included</h2>\n\n<p>The enumeration step is mechanical if you take it from the resolved OpenAPI description rather than from the code. Walk each operation and write down every assertion keyword that appears: <code>required</code>, <code>type</code>, <code>enum</code>, <code>minimum</code> and its exclusive twin, <code>minLength</code>, <code>maxLength</code>, <code>pattern</code>, <code>multipleOf</code>, <code>additionalProperties</code>. Each of those is one row. Two OpenAPI defaults add rows nobody writes by hand, because the description never mentions them.</p>\n\n<p>A Request Body Object's <code>required</code> field <strong>defaults to false</strong>. An operation whose <code>requestBody</code> declares a schema with five required properties but omits <code>required: true</code> has declared that sending no body at all is acceptable — invisible unless a case sends an empty request. A Parameter Object's <code>required</code> defaults to false everywhere except <code>in: path</code>, where the field is mandatory and its value must be <code>true</code>. A filter parameter you assumed was compulsory is optional until somebody writes it down.</p>\n\n<pre class=\"language-yaml\"><code class=\"language-yaml\"># Two rows the keyword walk would miss.\npaths:\n  /v1/orders:\n    post:\n      requestBody:\n        # No `required: true`, so an empty request is declared valid.\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/CreateOrder'\n  /v1/orders/{orderId}:\n    get:\n      parameters:\n        - name: orderId\n          in: path\n          required: true        # mandatory here, and must be true\n          schema: { type: string, format: uuid }\n        - name: include\n          in: query\n          # `required` omitted, so this defaults to optional\n          schema: { type: string, enum: [lines, payments] }</code></pre>\n\n<p><code>format</code> is the one keyword that should not become a rejection case without checking your stack first. Under the Format-Annotation vocabulary that OpenAPI 3.1 uses by default, <code>format</code> is collected as an annotation; implementations may treat it as an assertion, but that behaviour <strong>must be disabled by default</strong>. A case asserting that <code>format: uuid</code> rejects <code>\"not-a-uuid\"</code> is testing your validator's configuration, not the contract. Either enable assertion behaviour deliberately and record that you did, or express the rule with <code>pattern</code> so it is enforced by the structural vocabulary.</p>\n\n<h2 id=\"four-rule-families-four-oracles\">Four rule families need four different oracles</h2>\n\n<p>Rules that look alike in the description have genuinely different correct answers, and the work is choosing the right oracle for each family.</p>\n\n<p><strong>Structural rules</strong> — types, bounds, enums, unknown properties — are the only family where a validation error pointer is the oracle. The service knows which member of the instance failed, and the test should assert that location.</p>\n\n<p><strong>References to entities that do not exist</strong> are where most matrices adopt the wrong status. If a request to <code>POST /v1/orders</code> names a product SKU that was never created, the target resource of that request is <code>/v1/orders</code>, and it exists. RFC 9110 reserves 404 for the case where the origin server \"did not find a current representation for the target resource or is not willing to disclose that one exists\", and defines 422 for a request whose content type is understood and whose syntax is correct \"but it was unable to process the contained instructions\". A dangling reference inside a body is the second case; keep 404 for <code>GET /v1/products/{id}</code> on an ID that does not exist. The <a href=\"/tools/http-status\">HTTP Status Code Reference</a> settles the protocol meaning faster than arguing about it after the test fails.</p>\n\n<p><strong>Authorization rules</strong> need an oracle that compares two responses rather than inspecting one. RFC 9110 permits an origin server that wishes to hide the existence of a forbidden resource to answer 404 instead of 403, so the assertion is not \"this returns 403\" but \"this is indistinguishable from the response for an ID that does not exist\":</p>\n\n<pre class=\"language-ts\"><code class=\"language-ts\">test('another tenant’s invoice is indistinguishable from a missing one', async ({ request }) =&gt; {\n  // Same identity, two IDs: one real and owned by someone else, one fabricated.\n  const foreign = await request.get(`/v1/invoices/${otherTenantInvoiceId}`);\n  const absent = await request.get(`/v1/invoices/${randomUuid()}`);\n\n  expect(foreign.status()).toBe(absent.status());\n  expect(await foreign.text()).toBe(await absent.text());\n  // A body that names the resource, the owner, or the reason re-opens the leak\n  // even when both statuses match.\n});</code></pre>\n\n<p>One non-disclosure case per resource type is enough here; the full subject-object-action grid across identities, ownership and workflow state is a later part of this series.</p>\n\n<p><strong>State rules</strong> cannot be judged from a single request at all. A refund on an order that has not shipped is invalid now and valid later, so the oracle has two halves: the request is refused while the precondition is unmet, and the same request succeeds once the state changes. RFC 9110 describes 409 as a conflict \"with the current state of the target resource\" that \"the user might be able to resolve\" and resubmit, and the second half of the test is what proves the code was honest.</p>\n\n<pre class=\"language-ts\"><code class=\"language-ts\">test('refund is refused until the order ships, then allowed', async ({ request }) =&gt; {\n  const { id } = await createOrder(request, { state: 'PLACED' });\n\n  const early = await request.post(`/v1/orders/${id}/refunds`, { data: { amount: 1200 } });\n  expect(early.status()).toBe(409);\n\n  await advanceOrderTo(request, id, 'SHIPPED');\n\n  const allowed = await request.post(`/v1/orders/${id}/refunds`, { data: { amount: 1200 } });\n  expect(allowed.status()).toBe(201);\n});</code></pre>\n\n<h2 id=\"assert-the-failure-shape-not-the-sentence\">Assert the failure shape without asserting the sentence</h2>\n\n<p>Two specifications agree on this and are routinely ignored together. JSON Schema states outright that for errors, \"the specific wording for the message is not defined by this specification\" — implementations supply it, and they change it between releases. RFC 9457 says consumers \"SHOULD NOT parse the <code>detail</code> member for information\" and that extension members are the less error-prone route. The durable oracle is the structured location, never the prose.</p>\n\n<p>RFC 9457's own validation-error example carries an <code>errors</code> extension whose members hold a <code>detail</code> and a <code>pointer</code> locating the problem in the request content with a JSON Pointer. That pointer is what a test should assert.</p>\n\n<pre class=\"language-ts\"><code class=\"language-ts\">import { expect, type APIResponse } from '@playwright/test';\n\n/**\n * Asserts the failure contract: the declared media type, agreement between the\n * HTTP status and the advisory `status` member, the problem type, and the exact\n * JSON Pointer of the violation. Never the human-readable strings.\n */\nexport async function expectProblem(\n  response: APIResponse,\n  expected: { status: number; type: string; pointer?: string },\n) {\n  expect(response.status()).toBe(expected.status);\n  expect(response.headers()['content-type']).toContain('application/problem+json');\n\n  const problem = await response.json();\n\n  // RFC 9457 3.1.2: `status` is advisory, but a generator MUST use the same code\n  // in the response. A mismatch means an intermediary rewrote one of them.\n  if ('status' in problem) expect(problem.status).toBe(expected.status);\n\n  // RFC 9457 3.1.1: an absent `type` is assumed to be \"about:blank\", which\n  // identifies nothing. Requiring an explicit type is what makes the case stable.\n  expect(problem.type ?? 'about:blank').toBe(expected.type);\n\n  if (expected.pointer) {\n    const pointers = (problem.errors ?? []).map((e: { pointer?: string }) =&gt; e.pointer);\n    expect(pointers).toContain(expected.pointer);\n  }\n}</code></pre>\n\n<p>The <code>status</code> check looks redundant and is not. RFC 9457 requires a generator to use the same code in the actual HTTP response, and notes in its security considerations that generic HTTP software — proxies, load balancers, virus scanners — will not respect the code carried in the body. A disagreement between the two is evidence that something in the path rewrote the response, which is a finding rather than a flake.</p>\n\n<p>If your service returns a bare validator dump instead, its field names are not portable either. JSON Schema's own output units use <code>instanceLocation</code> and <code>keywordLocation</code>, the specification requires only one of the flag, basic or detailed structures, and popular validators name the same idea differently. Pick the shape your gateway emits, write it into the helper once, and assert against the helper.</p>\n\n<h2 id=\"prove-the-rejection-left-nothing-behind\">Prove the rejection left nothing behind</h2>\n\n<p>This is the assertion behind the second failure in the opening. A 422 tells you the request was refused. It does not tell you whether the handler had already reserved stock, written an audit row, incremented a rate-limit counter, or sent a confirmation email before the validation error surfaced. Each rejected case needs a probe chosen for what that operation would have touched:</p>\n\n<pre class=\"language-ts\"><code class=\"language-ts\">test('a rejected order reserves no stock and creates no order', async ({ request }) =&gt; {\n  const sku = await seedProduct(request, { stock: 5 });\n  const before = await stockLevel(request, sku);\n  const ordersBefore = await orderCount(request, { sku });\n\n  const response = await request.post('/v1/orders', {\n    data: { ...validOrder, sku, quantity: 0 },\n  });\n  await expectProblem(response, {\n    status: 422,\n    type: 'https://errors.example.com/validation',\n    pointer: '/quantity',\n  });\n\n  expect(await stockLevel(request, sku)).toBe(before);\n  expect(await orderCount(request, { sku })).toBe(ordersBefore);\n});</code></pre>\n\n<p>The probes that catch real defects read a durable counter: rows in a table exposed through a support endpoint, a ledger total, the length of a list response. A probe that re-reads the resource you just failed to create catches only the crudest partial write, because the object it looks for was never given an identifier.</p>\n\n<p>Multi-step operations are where partial writes live. If creating an order writes the order, reserves stock, then charges a card, a rule enforced at the third step has two earlier writes behind it. Cases whose rule is enforced late need a probe on every earlier step, and the surrounding transaction does not cover work done by another service.</p>\n\n<h2 id=\"the-matrix-is-a-file-and-it-costs-you\">The matrix is a reviewable file, and it costs you something</h2>\n\n<p>Keeping the enumeration in a data file rather than in 40 hand-written test bodies is what makes the coverage argument reviewable: someone can diff the matrix against the schema and see which keywords have no row.</p>\n\n<pre class=\"language-ts\"><code class=\"language-ts\">// contracts/create-order.negatives.ts\nexport const negatives = [\n  { rule: 'quantity/minimum', mutate: (o) =&gt; ({ ...o, quantity: 0 }),\n    status: 422, pointer: '/quantity', probe: 'stock' },\n  { rule: 'quantity/type', mutate: (o) =&gt; ({ ...o, quantity: 2.5 }),\n    status: 422, pointer: '/quantity', probe: 'stock' },\n  { rule: 'sku/required', mutate: ({ sku, ...rest }) =&gt; rest,\n    status: 422, pointer: '/sku', probe: 'none' },\n  { rule: 'currency/enum', mutate: (o) =&gt; ({ ...o, currency: 'XTS' }),\n    status: 422, pointer: '/currency', probe: 'none' },\n  { rule: 'body/required', mutate: () =&gt; undefined,\n    status: 400, pointer: undefined, probe: 'none' },\n  { rule: 'sku/reference', mutate: (o) =&gt; ({ ...o, sku: 'SKU-NEVER-CREATED' }),\n    status: 422, pointer: '/sku', probe: 'orders' },\n] as const;</code></pre>\n\n<p>The cost is real. Every row couples to your error contract, so a deliberate move of validation from 422 to 400 breaks every row at once — correct behaviour that still arrives as a wall of red. Boundary cases also break when the boundary legitimately moves, and that failure looks identical to a regression until someone reads the schema diff. Comparing the two schema versions in the <a href=\"/tools/json-diff\">JSON Diff tool</a> tells you within a minute whether the contract moved or the service did.</p>\n\n<p>The row that does not pay for itself is the fuzzed one. Broad random payloads find anomalies, but with no rule name attached to an input there is no oracle: a 500 from a mutated byte says something is wrong, not which promise the service failed to keep. Keep fuzzing if you have it, and keep it out of the matrix.</p>\n\n<h2 id=\"when-a-negative-case-is-quietly-lying\">When a negative case is quietly lying</h2>\n\n<h3 id=\"green-against-a-service-that-is-not-running\">Every case passes, including against a service that is not running</h3>\n\n<p>A family assertion is satisfied by connection failures, gateway errors and HTML error pages. Add one canary: a valid request that must return its documented success code. If the canary fails, the negative results in that run mean nothing. This is also why the exact status matters — a 503 from a crashed pod does not resemble a 422.</p>\n\n<h3 id=\"passes-locally-fails-behind-the-gateway\">The case passes locally and fails behind the gateway</h3>\n\n<p>Two validators are enforcing the contract and they disagree about which rejects first. A gateway validating against the published description rejects on <code>required</code> and <code>type</code> before the application sees the request, and its problem body will not carry your application's <code>type</code> URI. Decide which layer owns each rule family, then run the matrix through the same ingress path CI uses. A matrix that only runs against the application process is measuring a component, not the API.</p>\n\n<h3 id=\"the-pointer-is-empty\">The status is right and the pointer is empty</h3>\n\n<p>The service rejected the request without saying where. A caller cannot turn that into a field-level message, and it usually means the rejection came from a hand-written guard clause rather than from schema validation — in which case the other keywords on that property are unenforced. The <a href=\"/tools/json-schema\">JSON Schema Generator</a> turns a representative payload into a schema you can compare against what the service enforces, though only the description settles what it should enforce.</p>\n\n<h2 id=\"questions-that-arrive-with-the-first-matrix\">Questions that arrive with the first real matrix</h2>\n\n<h3 id=\"faq-400-or-422\">Should validation failures be 400 or 422?</h3>\n\n<p>Pick one per API and write it into the matrix rather than deciding case by case. RFC 9110 defines 400 for a request the server will not process \"due to something that is perceived to be a client error\", including malformed syntax, and 422 for content whose media type is understood and whose syntax is correct but whose instructions cannot be processed. Unparseable JSON is 400 under any reading. A schema violation in well-formed JSON fits 422 more precisely, though 400 stays defensible and many gateways emit it. A mix is what breaks the matrix, because then no test can assert an exact code.</p>\n\n<h3 id=\"faq-how-many-cases\">How many negative cases does one operation need?</h3>\n\n<p>One per assertion keyword in its schema, one per referenced entity, one per state precondition, and one non-disclosure case per resource type it exposes. A six-property create operation lands between twelve and twenty. Three means the schema has rules nobody enforces; ninety means you are testing combinations, and a combination cannot name the rule that failed.</p>\n\n<h3 id=\"faq-multiple-violations\">What should the service return when a request breaks three rules at once?</h3>\n\n<p>A contract decision to document, not a case to add. RFC 9457 recommends representing the most relevant or urgent problem when problems do not share a type, and its validation example shows the alternative: one problem type with an <code>errors</code> extension listing every occurrence. Assert whichever your API promises, and keep the rows single-mutation regardless — a row breaking three rules cannot say which one the service stopped enforcing.</p>\n\n<h3 id=\"faq-ai-generated-cases\">Can a model generate the matrix from the schema?</h3>\n\n<p>It is good at the enumeration and unreliable at the oracle. A resolved schema produces a usable first list of keywords and boundary values, often catching a <code>pattern</code> or <code>maxLength</code> you skipped. What it must not decide is the expected status for a family, the pointer your service emits, or which probe proves a side effect is absent — those come from the description, from an observed response, and from knowing what the handler writes. Take the generated list as rows and supply the three columns that make a row an oracle.</p>\n\n<h3 id=\"faq-additional-properties\">Is an unknown property in the request a negative case?</h3>\n\n<p>Only if the schema closes the object. With <code>additionalProperties</code> omitted the contract accepts unknown members, so a case asserting rejection tests a rule the API never declared. The better question is whether the unknown property is silently bound: if sending <code>{\"role\": \"admin\"}</code> to a profile update changes anything, that is a property-level authorization defect, and the probe belongs on the field it wrote.</p>\n\n<h2 id=\"primary-references\">Primary references</h2>\n\n<ul>\n<li><a href=\"https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9110, HTTP Semantics — Client Error 4xx</a>: the definitions this article relies on for 400 (§15.5.1), 403 including the permission to answer 404 to hide existence (§15.5.4), 404 (§15.5.5), 409 (§15.5.10), and 422, which RFC 9110 moved out of WebDAV into core HTTP (§15.5.21)</li>\n<li><a href=\"https://www.rfc-editor.org/rfc/rfc9457.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9457, Problem Details for HTTP APIs</a>: the <code>application/problem+json</code> media type, the advisory status member and the requirement that it match the response (§3.1.2), the absent-<code>type</code> default of <code>about:blank</code> (§3.1.1), the instruction not to parse <code>detail</code> (§3.1.4), extension members (§3.2), and the <code>errors</code>/<code>pointer</code> validation example</li>\n<li><a href=\"https://json-schema.org/draft/2020-12/json-schema-validation\" target=\"_blank\" rel=\"noopener noreferrer\">JSON Schema 2020-12 Validation</a>: <code>integer</code> matching any number with a zero fractional part (§6.1.1), inclusive <code>minimum</code> against exclusive <code>exclusiveMinimum</code> (§6.2.4–6.2.5), <code>required</code> checking property presence only (§6.5.3), and the Format-Annotation vocabulary requiring assertion behaviour to be off by default (§7.2.1)</li>\n<li><a href=\"https://json-schema.org/draft/2020-12/json-schema-core\" target=\"_blank\" rel=\"noopener noreferrer\">JSON Schema 2020-12 Core</a>: output units and their <code>instanceLocation</code> and <code>keywordLocation</code> keys (§12.3), the four output structures and the fact that only one of flag, basic or detailed is required (§12.2), and the statement that error wording is not specified (§12.3.4)</li>\n<li><a href=\"https://spec.openapis.org/oas/v3.1.1.html\" target=\"_blank\" rel=\"noopener noreferrer\">OpenAPI Specification 3.1.1</a>: Request Body Object <code>required</code> defaulting to false (§4.8.13.1) and Parameter Object <code>required</code> defaulting to false except in <code>path</code> (§4.8.12.2.1)</li>\n<li><a href=\"https://owasp.org/API-Security/editions/2023/en/0xa3-broken-object-property-level-authorization/\" target=\"_blank\" rel=\"noopener noreferrer\">OWASP API Security Top 10 2023 — API3:2023</a>: why an accepted unknown property is an authorization question, and the merge of the former excessive-data-exposure and mass-assignment categories</li>\n<li><a href=\"https://playwright.dev/docs/api-testing\" target=\"_blank\" rel=\"noopener noreferrer\">Playwright — API testing</a>: the <code>request</code> fixture and <code>APIResponse</code> members used in the examples, checked against Playwright 1.62</li>\n</ul>\n\n<h2 id=\"apply-this-now\">Apply this now</h2>\n\n<p>Take the operation in your API with the most required properties. Open its resolved schema, list every assertion keyword on it, and count how many of those keywords have a test that fails when the keyword is deleted. That count, not the number of negative tests, is your coverage.</p>\n\n<p>Then fix one row end to end: move its mutation onto the boundary, replace the family assertion with the exact status, assert the JSON Pointer instead of the message, and probe the durable counter the handler would have touched. Add the canary in the same commit. What should exist on the other side of that work is a keyword list with a covered-or-not column, a recorded pointer and probe for each case, and a settled answer to which status your API uses for malformed syntax and which it uses for schema violations. If that last one is still open, settle it before writing the second row — every row you add before then will need editing afterwards.</p>\n"}