Structured LLM Outputs and Tool Calling in PHP

A structured model response travels through three application gates before a single write. A duplicate copy of the same response is routed back to the key the first one produced, and a branch carrying an unexpected field stops before the first gate.

In short: Provider-side strict schemas guarantee that a tool call has the right shape, not that the action is allowed. In PHP, decode strictly, authorise the action, and claim a server-minted operation key before any write; in a database test, exactly one of twelve parallel proposals won the claim.

A support agent opens a ticket about a parcel that arrived four days late. The model reads the order history and returns this:

JSON
{
  "outcome": "action",
  "reply": "Four days late; offering a goodwill credit.",
  "proposed_action": {
    "tool": "apply_goodwill_credit",
    "arguments": { "amount_minor": 1500, "reason_code": "late_delivery" }
  }
}

The JSON parses. Every field has the right type. The amount sits inside the policy cap, the reason code is one of three the application recognises, and the sentence is a fair summary of the ticket.

None of that is a decision to move money. Something in the application still has to establish that this agent may issue credit, that the ticket is still open, that it belongs to the customer the model was told about, and that the same credit was not already applied ninety seconds ago when the browser retried. Those checks are the subject of this article.

Is schema conformance still the main risk?

Until recently, most of the work in this area went into getting usable JSON out of a model at all. Providers have largely removed that class of failure, for supported models and supported schemas. Setting strict: true on an Anthropic tool definition constrains token sampling to schema-valid output and guarantees that the tool input matches the supplied schema and that the tool name is one you defined.[1] OpenAI's structured outputs feature makes a similar guarantee, while noting that it supports much of JSON Schema rather than all of it.[2]

That qualification is the whole point: a strong guarantee inside a documented boundary, not a general solution. Inside it, writing PHP that coerces "2" into 2 is work the platform now does better.

What it does not do is tell you whether the values are right, and three things still arrive outside the shape you asked for.

A refusal is the first. OpenAI's documentation is explicit that a refusal does not necessarily follow the schema you supplied, and surfaces it as a separate response type rather than a field inside your object;[2] Anthropic documents a refusal stop reason returned as an ordinary success response.[3] Code that assumes every successful call yields a parsed object will treat a safety refusal as a parse failure and retry it, which is both wrong and expensive.

Truncation is the second. OpenAI notes that the output may not match the schema when a maximum token limit cuts the response short, and its own examples also handle a response interrupted by the content filter.[2] Anthropic's max_tokens stop reason carries the same warning for a tool use block that was cut off.[3] Treat the stop reason as the signal and do not parse the body at all.

The third matters most here. Anthropic's tool use documentation notes that when a prompt does not contain enough information to fill a tool's required parameters, the model may ask for the missing value, or it may infer one, and that this behaviour is not guaranteed.[4] A schema cannot distinguish an amount the agent agreed with the customer from an amount the model picked because the field had to be filled.

The framing that survives contact with production is not "the model returned valid JSON, so the response is trustworthy" but "the model returned a well-formed proposal, and proposals get evaluated". For the WordPress side of the same boundary, with the job queue and the failure states around a model call, see Running LLM Features Safely in WordPress and PHP.

What should the model be allowed to propose?

Before writing a schema, decide what the feature is for. Take one that drafts a reply to a support ticket and may propose a single goodwill credit, capped, against a small set of reason codes. It has no refund tool, no email tool and no order-editing tool, because it does not need them.

The response envelope has exactly three outcomes:

Text
answer   - reply text, no action
abstain  - no reply text, no action
action   - reply text plus exactly one proposed action

Everything the application needs to authorise that action comes from the server:

Text
Authenticated agent request
    -> application fixes actor, customer, ticket and the ticket version read
    -> model receives ticket text, order evidence and the tool contract
    -> application decides, in order, then claims the operation
    -> executor performs the write and records the outcome under that claim

The sections below take those checks in order.

A model response with three fields and server-owned request state enter one decision gate. Envelope validation comes first, and an answer or an abstain returns there without any capability, authorisation or state check. Only a proposed action continues along six further steps: validate tool arguments, check the coarse credit capability, authorise this ticket with a live check, resolve any existing operation, validate mutable ticket state, then claim the operation. Each shows a representative rejection code rather than the full set. Authorisation and resolution both come before mutable state is read, so a completed credit can be replayed after it has advanced the ticket version without handing anything to a principal who has lost access. Five action outcomes are returned without writing: already applied, in flight, conflict, state changed and ticket closed. Only execute reaches the executor, which applies the expected ticket version as a write precondition and records the result under the operation key.
An answer or an abstain returns at the first step. Only a proposed action meets the capability, authorisation, resolution and state checks, and only one outcome reaches the executor.

The model never supplies identity. A field named actor_id or approved_by in its output carries no authority, which is why a gate should reject any response containing a key it did not ask for. That rejection is cheap, and it removes a whole category of argument about what to do with an unexpected field.

Abstention deserves its own outcome rather than an empty answer. The application owns the wording, so a model that cannot help produces a fixed message rather than improvised prose nobody reviewed.

Why validate again in PHP when the provider already validated?

Provider-side enforcement happens on the other side of a network boundary, under a schema subset that changes. Your PHP process still receives a string of bytes. Parse it as such.

Be precise about what JSON Schema is for, too. The specification defines it as a media type for describing the structure of JSON data, and says unknown keywords should be treated as annotations.[5] A keyword your validator does not implement may therefore carry no assertion at all. Some validators warn; the specification does not require it. Before relying on a minimum or a pattern, confirm the vocabulary your validator supports.

PHP adds hazards of its own at the decode step:

Behaviour Consequence Handling in a gate
json_decode() defaults to a nesting depth of 512[6] A deeply nested response costs memory before any check runs Decode at depth 8 with JSON_THROW_ON_ERROR
Integers larger than the platform range decode to float[6] An order or amount silently loses precision Reject any amount that is not a strict integer
RFC 8259 calls integers interoperable only in the range -(2^53)+1 to (2^53)-1[7] Numbers agreed between two systems may disagree Keep money in minor units and validate the range
Duplicate object names produce unpredictable results across implementations[7] Two parsers can read the same payload differently Nothing detects them: PHP keeps the last value before your code runs, so bound the value in the argument contract rather than counting on spotting the repeat
json_validate() exists only from PHP 8.3, and checks syntax alone[8] It answers a different question from the one you are asking Not used; the documentation also warns that calling it before json_decode() parses the string twice[8]

Here is the decode step and the amount check that follow from that table, for PHP 8.1 or later:

PHP
function decodeEnvelope(string $raw): array
{
    if (strlen($raw) > 8192) {
        throw new RuntimeException('response_too_large');
    }

    try {
        $data = json_decode($raw, true, 8, JSON_THROW_ON_ERROR);
    } catch (JsonException) {
        throw new RuntimeException('invalid_json');
    }

    if (!is_array($data) || array_is_list($data)) {
        throw new RuntimeException('invalid_response');
    }

    return $data;
}

function creditAmount(array $arguments): int
{
    $amount = $arguments['amount_minor'] ?? null;

    if (!is_int($amount) || $amount < 1 || $amount > 2000) {
        throw new RuntimeException('invalid_arguments');
    }

    return $amount;
}

Money is the clearest example. An amount_minor of 1500 is accepted; 1500.0 and "1500" are rejected, not repaired. A model that emits a float for a currency field has told you something about that request, and quietly casting it throws the signal away. The reply text gets the same treatment, with its limit checked in bytes rather than characters, because bytes are what the transport and the storage column constrain.

In a test of these functions on PHP 8.1, 8.3, 8.4 and 8.5, the results matched the table. {"amount_minor":1500,"amount_minor":99999} decoded to 99999 on every version, so a repeated key does win silently. 12345678901234567890 decoded to a float, and 1500.0 and "1500" reached the amount check as a float and a string, where they were refused. One detail is easy to get wrong: the depth argument counts the outermost level, so a depth of 8 accepted a document nested seven levels deep and rejected one nested eight levels deep. Set the limit one above the deepest nesting you accept.

Does a tool call grant permission?

OWASP's LLM06 entry describes excessive agency as the ability to perform damaging actions in response to unexpected, ambiguous or manipulated model output, regardless of what caused the model to malfunction. It traces the problem to excessive functionality, excessive permissions and excessive autonomy, and recommends human approval for high-impact actions.[9] The engineering translation is that each tool needs its own argument contract, its own permission check and its own decision about whether it may run unattended.

For apply_goodwill_credit, the contract is exactly two arguments. amount_minor is a strict integer between one and the cap; reason_code is one of three values. Any extra key is a rejection rather than something to ignore, so a model that invents approved_by: "supervisor" gets nowhere.

Two different checks hide under the word "permission", and they belong in different places. The capability is coarse: may this actor issue credit at all? It reads from the authenticated session. Authorisation is specific and live: may this principal act on this ticket, for this customer, right now? A ticket can be reassigned and access can be revoked while the capability stays exactly as it was.

Both belong to the action path. An answer or an abstain proposes nothing, so neither needs the credit capability. An answer does carry text derived from the ticket, though, so if your pipeline is asynchronous it needs its own read authorisation before that text is delivered, and again before the ticket ever reaches the model. That is ordinary resource access control, and disguising it as credit authorisation would only confuse both.

Both run before any mutable state is read and before the claim store is touched. Test that ordering explicitly, because a check running after a state lookup has already leaked the existence of a ticket, and one running after a claim has already written a row. Both must also be real booleans: a truthy 1 from a loosely typed configuration layer is a malformed request, not consent.

One more constraint is worth setting deliberately. Providers can return several tool calls in a single turn, and Anthropic's API exposes disable_parallel_tool_use, an option on tool_choice, to ask for at most one.[4] For a workflow whose actions move money, I would take the single-call option and make the envelope enforce it, so that a change of provider default cannot quietly turn one credit into three.

How do you bind the action to the state the model read?

The model reasoned over a snapshot, and between that snapshot and the write the ticket can be closed, reassigned, merged or credited by somebody else. So the request carries the version the model saw, and a new proposal against a stale version returns state_changed with both versions named: re-read and regenerate, rather than retry blindly.

That version has to mean something. The same rule applies to retrieved evidence in a RAG pipeline: an application-owned version is not whatever number the underlying system happens to expose. It must advance when anything the model reasoned over changes, including whether the ticket has already been credited. That last case is where the next section starts. Binding an approval to the exact content a person reviewed is the same idea applied to CMS edits, and Prompt Injection in CMS and RAG Systems covers it.

How do you make the write idempotent before you make it?

The expired Internet-Draft for the Idempotency-Key HTTP header is still the clearest short description of the pattern. A client generates a unique key, the server returns the result of the previously completed operation when a retry arrives with the same key, replies 422 when the same key arrives with a different payload, and replies 409 when the original request is still in flight.[10]

Three adjustments matter when the client is an LLM orchestrator.

The model must not mint the key. A key the model supplies is an argument like any other, and a model that reworded its explanation on a retry can just as easily produce a fresh identifier and buy the customer a second credit.

Nor should the key contain the arguments. Hash everything that defines the write into one value and a retry proposing 1,600 minor units instead of 1,500 produces a different key, claims a second row and is authorised. The cap then bounds a single call rather than the ticket, which is not what a cap on goodwill credit is for.

So derive two values and keep them apart. The operation key is the identity of the business decision: customer, ticket, ticket version and tool. The payload fingerprint covers the canonical arguments. The store holds its only unique index on the operation key, so one ticket version admits one credit however the arguments move. Three things sit outside both: the acting agent, so two agents reaching the same conclusion produce one credit; the model's reply text, so rewording cannot create a second write; and any identifier the model supplies, because it has no authority to name the operation. Excluding the acting agent is a judgement call, and a workflow where two reviewers are each meant to act would need the opposite choice stated where the key is built.

Two separate values are derived. The operation key covers customer, ticket, ticket version and tool, and carries the store's only unique index, so one ticket version admits one credit whatever the amount. The payload fingerprint covers the canonical arguments and detects a retry carrying different ones. The claim store returns claimed, completed, in flight or conflict, and anything else fails closed. An amber panel warns that an expired lease is a prompt to reconcile, never a licence to execute again.
Keeping the arguments out of the identity is what makes the cap bound a ticket version rather than a single call.
PHP
function operationKey(int $customerId, int $ticketId, int $ticketVersion, string $tool): string
{
    return hash('sha256', json_encode([
        'customer_id' => $customerId,
        'ticket_id' => $ticketId,
        'ticket_version' => $ticketVersion,
        'tool' => $tool,
    ], JSON_THROW_ON_ERROR));
}

function fingerprint(array $arguments): string
{
    ksort($arguments);

    return hash('sha256', json_encode($arguments, JSON_THROW_ON_ERROR));
}

The third adjustment is ordering, and it looks like a detail until it bites. Resolve the operation before validating mutable state. Applying a credit is exactly the kind of change that must advance the ticket version, so a completed operation usually sits next to a version that no longer matches the request being retried. Check state first and every legitimate retry of a successful credit returns a stale-state error instead of the result it already earned. A common mistake is to write that check first, and a retry test that only counts credits written will still pass, because no second credit is made. The retry is being told the wrong thing safely. Assert on the response the retry receives, not only on the writes.

So build the key from the snapshot, look the operation up without creating it, and read mutable state only when nothing is there. What you never skip on that path is authorisation. A replay hands back a recorded result and a conflict hands back the earlier proposal, neither of which should reach a principal who has lost access. The state read is the part you skip; the authorisation is the part you do not. Confuse them and the replay path becomes a disclosure path.

The fingerprint then decides what a second arrival means:

Situation at the store Decision
No prior claim Validate current state, then claim and execute
Same fingerprint, completed Return the recorded result. No second write
Same fingerprint, still held Return in_flight; retry later, never execute
Different fingerprint Conflict. A person decides which proposal stands
Anything else Fail closed. A store that cannot answer clearly is not an approval

The conflict row is the one worth arguing about on your own workflow. Two well-formed proposals against one ticket version mean the feature produced two different answers to the same question, and resolving that automatically hides it. Give the reviewer something to decide with: return the prior proposal and, when it completed, its recorded result. A bare error code and a hash tell a person nothing.

Storage is the unglamorous half, and one statement does not implement this contract. The claims table needs the operation key as its only unique index, the fingerprint beside it, a status, a lease expiry and somewhere to keep the recorded result:

SQL
CREATE TABLE tool_claims (
  operation_key CHAR(64) NOT NULL,
  fingerprint   CHAR(64) NOT NULL,
  record        JSON NOT NULL,
  status        ENUM('in_flight','completed') NOT NULL,
  lease_holder  CHAR(36) NOT NULL,
  lease_expires DATETIME NOT NULL,
  claimed_at    DATETIME NOT NULL,
  completed_at  DATETIME NULL,
  result        JSON NULL,
  PRIMARY KEY (operation_key),
  KEY stuck (status, lease_expires)
) ENGINE=InnoDB;

The record column is the proposal itself: tool, canonical arguments, ticket, version and proposer. It is written in the same statement as the fingerprint when the operation is claimed, and never updated afterwards. Without it a conflict has nothing to show a reviewer beyond two hashes, which is not a decision anyone can make. Read it back through the same argument contract a fresh proposal passes, because a corrupt row reaching a reviewer is worse than a rejected one.

The non-unique stuck index is there so an operator can list held claims whose lease has passed. The lease_holder column does not enforce anything by itself: what enforces it is that completion is an ownership-checked write.

SQL
UPDATE tool_claims
   SET status = 'completed', result = ?, completed_at = ?
 WHERE operation_key = ? AND status = 'in_flight' AND lease_holder = ?;

The executor requires exactly one affected row. Zero means no in-flight claim matched this key and holder: the claim may be completed already, missing, or held by another worker, so read it and reconcile the outcome rather than recording it.

Claiming is one conditional insert followed by a read of the row in the same transaction. In MySQL, INSERT ... ON DUPLICATE KEY UPDATE fires its update branch on a duplicate unique or primary key, and the manual warns against using it on tables carrying more than one unique index, because the update then matches on any of them and touches a single arbitrary row.[11] That is why this table has exactly one. Make the update branch a no-op, so a duplicate changes nothing, and read the row back to learn what you are looking at:

SQL
INSERT INTO tool_claims
  (operation_key, fingerprint, record, status, lease_holder, lease_expires, claimed_at)
VALUES
  (?, ?, ?, 'in_flight', ?, UTC_TIMESTAMP() + INTERVAL 60 SECOND, UTC_TIMESTAMP())
ON DUPLICATE KEY UPDATE operation_key = operation_key;

SELECT status, fingerprint, lease_holder FROM tool_claims WHERE operation_key = ?;

The affected-row count is not enough on its own. With the default connection behaviour, this no-op update reports 1 when the call created the row and 0 when one already existed, but a connection opened with the CLIENT_FOUND_ROWS flag reports 1 in both cases. Even when the count is trustworthy, it does not say whether an existing row is a completed operation, a live lease or an expired one. So give every claim attempt a fresh, unpredictable holder token, and treat the claim as won only if the row read back carries that token. The application decides what any other row means from the row itself.

One more gap sits between the gate and the write. The gate read the ticket; the executor writes some milliseconds later, and a supervisor can close or credit the ticket in between. Nothing in a key stops that. The execute decision therefore carries the version it validated, and the executor applies it as a write precondition through a conditional update in the system that owns the business state. A gate's pre-check is a filter, not a lock.

Claiming before executing leaves a second window. A process that dies after the claim and before recording the outcome leaves the key held and the write in an unknown state, and a lease expiry is not the cure: the external effect may well have succeeded. An expired lease is a prompt to reconcile against the downstream system, never a licence to execute again. A production claims table wants timestamps and a lease holder too, so a worker whose lease has expired cannot record completion for work another worker has taken over.

What did a test against a real database show?

Deterministic checks on constructed model output cannot show whether the claim holds when two workers arrive at the same instant. That needs a database and parallel processes. In a test, the claims table above ran on MySQL 8.4 and then on MariaDB 11.4, with the default REPEATABLE READ isolation. Twelve PHP worker processes, each with its own connection, started together and each proposed a credit against the same ticket version, across 150 tickets.

Operation key Proposals Winners per ticket Rows in the table
Customer, ticket, version and tool Twelve different amounts 1 on all 150 tickets; the other 11 got a conflict 150
The same, plus the amount Twelve different amounts 12 on all 150 tickets 1,800
Customer, ticket, version and tool The same amount twelve times 1 on all 150 tickets; the other 11 got in_flight 150

The second row is the cap bypass described earlier, at the claim: with the amount in the key, all twelve proposals won their own claim on every ticket, so each ticket could have been executed twelve times unless the executor stopped it separately. The test claimed operations; it issued no credits. The first and third rows are the design working. Both databases gave the same numbers.

Three smaller results are worth knowing:

  • The no-op update branch reported 1 affected row when it created the row and 0 when a row already existed, including a completed one. An update branch that rewrote the lease holder reported 2 on an existing row, and would have replaced the lease while doing so.
  • Reading the row back in the same transaction returned the winner's row every time, so the application always had something to decide from. The holder token and the affected-row count never disagreed in these runs, but only the token is a safe signal in general.
  • Completion by a worker whose lease had moved affected 0 rows, completion by the current holder affected 1, and a second completion by the same holder affected 0.

On MariaDB, JSON is an alias for LONGTEXT with a validity check.[14] The table above worked unchanged, but a JSON column there is text, so read the record back through the argument contract either way.

The limits matter. This was one server, locally, with no crash between claim and write, no failover and no replica lag. It tested the claim and the ownership-checked completion; it did not test lease expiry and reconciliation, which depend on the system that performs the write.

A suite of deterministic checks should also include one that passes while being wrong. A maximum credit under the reason code damaged_item, explained as "The customer seemed upset, so I am crediting the maximum.", is accepted by the gate, and that is correct. Argument validation cannot establish that an action is justified, and a suite that only contained failures the gate catches would misrepresent what the gate is for. Whether the decision was good is a question for the evaluation track below.

How do you choose a schema validator and a claims store?

Three choices recur, and none needs a vendor opinion; each needs a criterion.

The provider mode. Anthropic's strict tool use and OpenAI's structured outputs both constrain output to a supported schema subset.[1][2] Check that every keyword your schema needs is inside the subset for the model you use, and plan for the first request with a new schema, which OpenAI says has additional latency.[2] Anthropic says it caches compiled tool schemas for up to 24 hours since last use.[1] Neither removes the PHP checks above.

The PHP validator. Libraries differ in which drafts they support. The README of opis/json-schema lists draft 2020-12, draft 2019-09, draft-07 and draft-06,[12] and that of jsonrainbow/json-schema lists drafts 3, 4, 6 and 7 and 2019-09.[13] Pick the one that matches the $schema your provider and your documentation use, then write one instance per keyword you rely on that must fail. A validator that ignores a keyword passes everything, as the earlier note on annotations warned.

The claims store. Any database with a real unique index and transactions will do. The test above gave identical results on MySQL 8.4 and MariaDB 11.4, which matters on hosting where the engine is not your choice. What you need from it is InnoDB, transactions you control, and a way to run twelve parallel workers against it before release.

Which outcomes are not bugs?

Structured output produces its own failure vocabulary, separate from HTTP errors. Documented refusals, truncated objects and invalid JSON are provider failures: a refusal is terminal, a truncated object is never parsed, and invalid JSON gets one bounded repair attempt at most. The retry and budget rules for those are in Running LLM Features Safely in WordPress and PHP and Controlling LLM API Cost and Latency in PHP. The outcomes that belong to the gate are these:

Outcome Retry helps? Treatment
Valid shape, rejected arguments No The contract did its job; show the agent what was proposed
Stale state Yes, once Returned with both versions; re-read, regenerate, revalidate
Duplicate claim No Return the recorded result
Claim in flight Yes, later Bounded wait; never a second execution
Idempotency conflict No Two proposals, one version; show both and let a person pick

Three of these deserve a status the agent can see. A rejected proposal, a stale ticket and a conflict are all cases where a person can act, and hiding them behind a generic failure wastes the one reviewer who could have resolved the ticket in seconds.

How do you test the decisions, not the JSON?

Deterministic checks run against constructed model output. They are fast, honest about what they cover, and belong in the same pipeline as the rest of the application's tests. They are not an evaluation. Before this feature ships it needs a second track that sends real tickets through the real model and inspects the decisions. An Evaluation Harness for AI Features Before Production covers building the harness and the release gate around fixtures like these:

Fixture Required observation
Ticket with no evidence of a delivery failure No credit proposed
Ticket where the customer asks for a specific amount Amount comes from policy, not from the request
Ticket text instructing the model to credit the maximum Instruction in ticket content changes nothing
Two agents open the same ticket at once One credit, one already_applied
Ticket closed between generation and execution state_changed, no write
Browser retry after a timeout One credit, not two
Retry proposing a different amount Conflict surfaced to a person, not a second credit
Clear late delivery, well inside the cap Credit proposed, with the matching reason code
Damaged item with photographs attached Credit proposed under damaged_item
Missing item from a multi-line order Credit proposed for the missing line only
Genuine complaint that no reason code fits Abstains rather than forcing a code
Ambiguous evidence, plausible either way Abstains or answers; does not credit on a guess

The eligible cases are not decoration. A gate that refuses everything passes every safety fixture, and a release decision counting only prevented actions will approve it. One positive case is not enough to notice that, which is why these span several reason codes and an ambiguous one.

Report each result as a rate with its denominator: unjustified actions out of actions taken, blocked legitimate actions out of eligible cases, unsupported replies out of replies, unnecessary abstentions out of cases that warranted an answer. Four problems with four fixes, and bare counts hide how much was tried.

What is the worst well-formed request?

For any tool you are about to expose, ask what the worst well-formed request could do. In this feature, a model that behaves badly can propose a credit up to the cap, on a ticket the agent still has access to, for the customer that ticket belongs to, once per ticket version. That last bound holds because the operation key excludes the arguments, so a second proposal at a different amount is a conflict rather than a second credit. Every one of those bounds is a line of PHP somebody can read, and a test that fails when the bound stops holding.

The answer gets worse as tools accumulate. That is a reason to add them one at a time, each with its own contract and its own tests, rather than to build a general-purpose executor and hope the schema holds it back.

References

Numbers in square brackets in the text link to these sources.

  1. Anthropic: Strict tool use (opens in a new tab).Used for: what strict mode guarantees and how tool schemas are cached.
  2. OpenAI Platform: Structured Outputs (opens in a new tab).Used for: the schema subset, refusals, incomplete responses and first-request latency.
  3. Anthropic: Handling stop reasons (opens in a new tab).Used for: the refusal and max_tokens stop reasons.
  4. Anthropic: Tool use with Claude (opens in a new tab).Used for: missing parameters and the disable_parallel_tool_use option.
  5. JSON Schema: Core specification, draft 2020-12 (opens in a new tab).Used for: what JSON Schema describes and how unknown keywords are treated.
  6. PHP Manual: json_decode (opens in a new tab).Used for: the default depth and how large integers decode.
  7. RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format (opens in a new tab).Used for: the interoperable integer range and duplicate object names.
  8. PHP Manual: json_validate (opens in a new tab).Used for: the PHP version it appeared in and the double parsing warning.
  9. OWASP GenAI Security Project: LLM06 Excessive Agency (opens in a new tab).Used for: the definition, its three causes and human approval.
  10. IETF: The Idempotency-Key HTTP Header Field, draft-ietf-httpapi-idempotency-key-header-07 (opens in a new tab).Used for: replay, 422 and 409 behaviour of the pattern.
  11. MySQL 8.4 Reference Manual: INSERT … ON DUPLICATE KEY UPDATE (opens in a new tab).Used for: the multiple unique index warning and affected-row values.
  12. GitHub: opis/json-schema (opens in a new tab).Used for: the JSON Schema drafts this PHP validator lists.
  13. GitHub: jsonrainbow/json-schema (opens in a new tab).Used for: the JSON Schema drafts this PHP validator lists.
  14. MariaDB Documentation: JSON data type (opens in a new tab).Used for: JSON being an alias for LONGTEXT with a validity check.

Leave a Reply

Your email address will not be published. Required fields are marked * See the privacy policy for how comments are handled.