JSON Schema for Login Forms
Part of the JSON Toolkit{"required":["password"],"anyOf":[{"required":["email"]},{"required":["username"]}]}the part that lets someone log in with either field — see below for the complete schema with password and rememberMe
Explanation
A login form has a narrower job than a registration form — it just needs an identifier and a password, not a full profile. But it has one wrinkle registration doesn't: plenty of apps let someone log in with either their email or their username, and JSON Schema's required keyword alone can't express "at least one of these two." That's what anyOf is for:
{
"type": "object",
"properties": {
"email": { "type": "string", "format": "email" },
"username": { "type": "string", "minLength": 3, "maxLength": 20, "pattern": "^[a-zA-Z0-9_]+$" },
"password": { "type": "string", "minLength": 1 },
"rememberMe": { "type": "boolean" }
},
"required": ["password"],
"anyOf": [
{ "required": ["email"] },
{ "required": ["username"] }
],
"additionalProperties": false
}password is required unconditionally, since every login attempt needs one. The anyOf array then adds a second, separate condition: the object must also satisfy at least one of two sub-schemas, one requiring email and one requiring username. An object with both present still passes — anyOf only demands one match, not exactly one (that's what oneOf is for, and it would be the wrong choice here: nothing's wrong with a client that happens to send both).
Notice password only has minLength: 1 here, not the minLength: 8 a registration schema typically enforces. A login endpoint's job is to check that a password was submitted at all, then compare it against the stored hash — re-running today's complexity rules against a password that was created under an older, looser policy would lock out legitimate users who never did anything wrong.
additionalProperties: false still earns its place, but for a different reason than on a registration form. There's less of a mass-assignment risk here — login doesn't write client-supplied fields into a new record, it just checks credentials against ones already stored — but rejecting unexpected fields still catches a typo'd key (usernme) or a stray debug field before it silently gets ignored by the handler instead of flagged as a bug. If your form legitimately sends something extra, like a CAPTCHA token or a device fingerprint, add it to properties explicitly rather than loosening this to true.
What this schema can't do: tell you whether the password is correct. That check requires looking up the account and comparing against a stored hash — schema validation only confirms the request has the right shape to attempt that lookup at all, the same boundary described in JSON Schema for User Registration.
For the vocabulary behind keywords like anyOf and additionalProperties, see What Is JSON Schema?. For more on how oneOf/anyOf differ in practice, see JSON Schema Examples. For validating a request body with an array of items rather than a flat form, see JSON Schema for API Requests.
Valid examples
{"email":"jane@example.com","password":"correcthorsebattery"}Logging in with email — satisfies the anyOf branch that requires email.
{"username":"jane_doe","password":"hunter2","rememberMe":true}Logging in with username instead, plus the optional rememberMe flag.
{"email":"jane@example.com","username":"jane_doe","password":"x"}Both identifiers present — anyOf only needs one match, so having both is still fine, and a one-character password passes since minLength here is just 1.
Invalid examples
{"password":"correcthorsebattery"}Neither email nor username — fails anyOf, since neither branch is satisfied.
{"email":"jane@example.com"}No password field — fails required, which demands it unconditionally regardless of which identifier is used.
{"email":"jane@example.com","password":"x","captchaToken":"abc"}An unlisted captchaToken field — additionalProperties: false rejects it; add it to properties explicitly if your form actually sends one.