ConceptsValidation
Validation
Relax server-side validation on a write with the validation-level bit flags. Which bits are public, what each does, and how to add them up.
Answer
validation-level relaxes validation on create, update and batch-modify. It takes a bit mask, member names or a comma list: 2 skips unchanged fields, 16 allows links to deleted records, 32 lets an update empty a required field, 512 allows duplicates in a unique field. Use 2 to update records that are already invalid.
Every write is validated: required fields, e-mail formats, links to other records, unique values.
validation-level turns some of those checks off for one request. The usual reason to use it is
a sync that writes back records which were already invalid when it read them.
How it works
validation-level is a query parameter of POST /entities/{Entity} (create),
PATCH /entities/{Entity}/{id} (update) and POST /entities/{Entity}/batch-modify. On
batch-delete and batch-restore it is refused with HTTP 400, code 118. In GraphQL, the create,
update, bulk and convert… mutations take validationLevel as a list of names:
validationLevel: [SKIP_UNCHANGED_FIELDS]. Leave the argument out for full validation: an
explicit validationLevel: null on bulkUpdateAccount fails with code 1000 ERROR_UNKNOWN,
even for a valid update.
The value is a bit mask. Send it as a number, by name, or as a comma list of either; a list is combined with OR, so a repeated value does no harm:
| Form | Example | Means |
|---|---|---|
| Number | validation-level=18 |
2 + 16 |
| Names | validation-level=SKIP_ |
The same |
| Numbers | validation-level=2,16 |
The same |
Add the bits up to combine them, and take a number apart the same way: 416 is
256 + 128 + 32, not 4 and 16 (that is 20).
The bits
| Bit | Name | What it does |
|---|---|---|
| 0 | FULL |
Every check; the default |
| 2 | SKIP_ |
A batch-modify update goes through on a record that already holds an invalid value it does not change, whether it re-sends that value or leaves it out. On create, it also lets a contact through without first_ or last_. |
| 4 | SKIP_ |
Skips the rules an admin sets on forms |
| 8 | SKIP_ |
Keeps a created or modified more than one day in the future, which a write otherwise ignores (dates) |
| 16 | SKIP_ |
A record may link to a deleted record, such as a parent_ of a deleted account |
| 32 | SKIP_ |
An update may set a required field to empty, such as "name": "" by PATCH |
| 64 | SKIP_ |
Accepted, and part of SKIP_ |
| 128 | SKIP_ |
Skips the checks of step checklists |
| 254 | SKIP_ |
Bits 2 to 128 together. Only this full set lets through an invalid e-mail format or a dropdown value from another field's options. |
| 512 | SKIP_ |
Allows a duplicate in a field that must be unique. Not part of SKIP_: add it, 766. |
- No bit lets a create through without its name: an account without
nameis refused with HTTP 422, even withSKIP_ALL. - A contact needs
first_nameorlast_name, although the published spec marks onlyowner_idas required: without both, the create answers HTTP 422, code 119. - Other creates need more than the published spec marks as required, and answer HTTP 422
without it: an opportunity needs
account_relationsand avaluewithcurrency_id(create an opportunity), a product needsunit_symbol, and itsskumust be unique, or the create answers HTTP 400 (products and line items), and a lead needsowner_idorunit_id(lead from a web form). - Send only the bits in this table. A value that carries either of two internal bits is refused
with HTTP 400, code 117
ERROR_PARAMETER_INVALID, and nothing is written. That holds on create, update andbatch-modify, when the bit is added to public bits or sent in a comma list, and for-1, which sets every bit. Other bits that name no member, such as1or1024, are accepted without an error.
Update records that are already invalid
A PATCH ignores the fields it does not change, even when you send them with their current
value. A batch-modify update does not: it validates the whole record, fields you do not send
included. A batch-modify therefore fails on a record that holds an invalid value, whether the
update sends that value unchanged or leaves it out.
The sample creates such a record, an account whose email1 is not-an-email. It stands in for
old data and uses SKIP_ALL (254) to get past the e-mail check; do not do that in your own code:
# Stands in for a record that is already invalid, as older data often is: validation-level=254
# (SKIP_ALL, every skip bit from 2 to 128) lets the invalid e-mail address in. Do not do this
# in your own code.
account=$(curl -sS --fail-with-body "$COEVERA_BASE/entities/Accounts?validation-level=254" \
-u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
-H "Content-Type: application/json" \
-d @- <<JSON
{ "name": "Northwind Analytics", "owner_id": "$OWNER_ID", "email1": "not-an-email" }
JSON
)
ACCOUNT_ID=$(jq -er '.data.id' <<<"$account")// Stands in for a record that is already invalid, as older data often is: validation-level=254
// (SKIP_ALL, every skip bit from 2 to 128) lets the invalid e-mail address in. Do not do this
// in your own code.
const created = await call("/entities/Accounts?validation-level=254", {
name: "Northwind Analytics",
owner_id: ownerId,
email1: "not-an-email",
});
if (created.status >= 300) throw new Error(`HTTP ${created.status} ${created.body.name}: ${created.body.message}`);
const accountId: string = created.body.data.id;# Stands in for a record that is already invalid, as older data often is: validation-level=254
# (SKIP_ALL, every skip bit from 2 to 128) lets the invalid e-mail address in. Do not do this
# in your own code.
created = session.post(
f"{BASE}/entities/Accounts",
params={"validation-level": 254},
json={"name": "Northwind Analytics", "owner_id": owner_id, "email1": "not-an-email"},
timeout=60,
)
if not created.ok:
sys.exit(f"HTTP {created.status_code}: {created.text}")
account_id = created.json()["data"]["id"]# Stands in for a record that is already invalid, as older data often is: SKIP_ALL lets the
# invalid e-mail address in. Do not do this in your own code.
ACCOUNT_ID=$(jq -n --arg owner "$OWNER_ID" '{
query: "mutation Existing($input: CreateAccountInput!) { createAccount(input: $input, validationLevel: [SKIP_ALL]) { account { id } } }",
variables: { input: { name: "Northwind Analytics", ownerId: $owner, email1: "not-an-email" } }
}' | gql | jq -er '.data.createAccount.account.id')// Stands in for a record that is already invalid, as older data often is: SKIP_ALL lets the
// invalid e-mail address in. Do not do this in your own code.
const created = await gql(
`mutation Existing($input: CreateAccountInput!) {
createAccount(input: $input, validationLevel: [SKIP_ALL]) { account { id } }
}`,
{ input: { name: "Northwind Analytics", ownerId, email1: "not-an-email" } },
);
if (created.errors) throw new Error(`Unexpected GraphQL errors: ${JSON.stringify(created.errors)}`);
const accountId: string = created.data.createAccount.account.id;# Stands in for a record that is already invalid, as older data often is: SKIP_ALL lets the
# invalid e-mail address in. Do not do this in your own code.
created = gql(
"""
mutation Existing($input: CreateAccountInput!) {
createAccount(input: $input, validationLevel: [SKIP_ALL]) { account { id } }
}
""",
{"input": {"name": "Northwind Analytics", "ownerId": owner_id, "email1": "not-an-email"}},
)
if created.get("errors"):
sys.exit(f"Unexpected GraphQL errors: {json.dumps(created['errors'])}")
account_id = created["data"]["createAccount"]["account"]["id"]A batch-modify that changes only city, and sends the unchanged email1 with it, is refused:
# The sync sends the whole record back with one change, the city. The unchanged, invalid
# e-mail address fails the update: HTTP 422, and nothing is written. No --fail-with-body:
# the 422 is the expected answer here.
UPDATE="[{ \"id\": \"$ACCOUNT_ID\", \"email1\": \"not-an-email\", \"city\": \"Seattle\" }]"
curl -sS -w '\nHTTP %{http_code}\n' "$COEVERA_BASE/entities/Accounts/batch-modify" \
-u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
-H "Content-Type: application/json" \
-d "$UPDATE"// The sync sends the whole record back with one change, the city. The unchanged, invalid
// e-mail address fails the update: HTTP 422, and nothing is written.
const update = [{ id: accountId, email1: "not-an-email", city: "Seattle" }];
const strict = await call("/entities/Accounts/batch-modify", update);
console.log(`Full validation: HTTP ${strict.status}`, JSON.stringify(strict.body, null, 2));# The sync sends the whole record back with one change, the city. The unchanged, invalid
# e-mail address fails the update: HTTP 422, and nothing is written.
update = [{"id": account_id, "email1": "not-an-email", "city": "Seattle"}]
strict = session.post(f"{BASE}/entities/Accounts/batch-modify", json=update, timeout=60)
print(f"Full validation: HTTP {strict.status_code}", json.dumps(strict.json(), indent=2))# sync [LEVEL]: the update request, the whole record with one change, the city. LEVEL is a JSON
# list of validation levels; without it, full validation. Leave validationLevel out rather than
# sending null: an explicit null is answered with an unexpected error (code 1000).
sync() {
jq -n --arg id "$ACCOUNT_ID" --argjson level "${1:-null}" '{
query: "mutation Sync($input: [CreateOrUpdateAccountInput!]!, $validationLevel: [ValidationLevel!]) { bulkUpdateAccount(input: $input, validationLevel: $validationLevel) { result { updated errors { index code message } } } }",
variables: ({ input: [{ id: $id, email1: "not-an-email", city: "Seattle" }] }
+ if $level then { validationLevel: $level } else {} end)
}'
}
# The unchanged, invalid e-mail address fails the update: HTTP 200 with errors and no result,
# and nothing is written.
sync | gql
echo// The sync sends the whole record back with one change, the city. The unchanged, invalid
// e-mail address fails the update: HTTP 200 with errors and no result, and nothing is written.
const UPDATE = `mutation Sync($input: [CreateOrUpdateAccountInput!]!, $validationLevel: [ValidationLevel!]) {
bulkUpdateAccount(input: $input, validationLevel: $validationLevel) {
result {
updated
errors { index code message }
}
}
}`;
const update = [{ id: accountId, email1: "not-an-email", city: "Seattle" }];
const strict = await gql(UPDATE, { input: update });
console.log("Full validation:", JSON.stringify(strict, null, 2));# The sync sends the whole record back with one change, the city. The unchanged, invalid
# e-mail address fails the update: HTTP 200 with errors and no result, and nothing is written.
UPDATE = """
mutation Sync($input: [CreateOrUpdateAccountInput!]!, $validationLevel: [ValidationLevel!]) {
bulkUpdateAccount(input: $input, validationLevel: $validationLevel) {
result {
updated
errors { index code message }
}
}
}
"""
update = [{"id": account_id, "email1": "not-an-email", "city": "Seattle"}]
strict = gql(UPDATE, {"input": update})
print("Full validation:", json.dumps(strict, indent=2)){
"code": 40000,
"name": "ERROR_ENTITY_VALIDATION",
"message": "\nInvalid email in field email1: \"not-an-email\"",
"entity_id": "0a000000-0000-4000-a000-000000000001",
"entity_name": "Account",
"entity_errors": [
{
"code": 117,
"name": "ERROR_PARAMETER_INVALID",
"message": "Invalid email in field email1: \"not-an-email\""
}
],
"field_errors": [],
"step_checklist_errors": [],
"entity_index": null,
"http_status": 422,
"success": false
}{
"errors": [
{
"message": "\nInvalid email in field email1: \"not-an-email\"",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"bulkUpdateAccount"
],
"api_error": {
"code": 40000,
"name": "ERROR_ENTITY_VALIDATION",
"message": "\nInvalid email in field email1: \"not-an-email\"",
"entity_id": "0a000000-0000-4000-a000-000000000001",
"entity_name": "Account",
"entity_errors": [
{
"code": 117,
"name": "ERROR_PARAMETER_INVALID",
"message": "Invalid email in field email1: \"not-an-email\""
}
],
"field_errors": [],
"step_checklist_errors": [],
"entity_index": null
}
}
],
"data": {
"bulkUpdateAccount": null
}
}Show all 36 linesShow less
The same update with validation-level=2 goes through. The unchanged e-mail is not checked
again, and a changed field is still validated as usual:
# The same update with validation-level=2 (SKIP_UNCHANGED_FIELDS): only the city is validated.
curl -sS --fail-with-body "$COEVERA_BASE/entities/Accounts/batch-modify?validation-level=2" \
-u "$COEVERA_API_KEY:$COEVERA_API_SECRET" \
-H "Content-Type: application/json" \
-d "$UPDATE"
echo// The same update with validation-level=2 (SKIP_UNCHANGED_FIELDS): only the city is validated.
const relaxed = await call("/entities/Accounts/batch-modify?validation-level=2", update);
console.log(`validation-level=2: HTTP ${relaxed.status}`, JSON.stringify(relaxed.body, null, 2));
if (relaxed.status !== 200) throw new Error(`Expected HTTP 200, got ${relaxed.status}`);# The same update with validation-level=2 (SKIP_UNCHANGED_FIELDS): only the city is validated.
relaxed = session.post(
f"{BASE}/entities/Accounts/batch-modify",
params={"validation-level": 2},
json=update,
timeout=60,
)
print(f"validation-level=2: HTTP {relaxed.status_code}", json.dumps(relaxed.json(), indent=2))
if relaxed.status_code != 200:
sys.exit(f"Expected HTTP 200, got {relaxed.status_code}")# The same update with SKIP_UNCHANGED_FIELDS: only the city is validated.
relaxed=$(sync '["SKIP_UNCHANGED_FIELDS"]' | gql)
echo "$relaxed"
if jq -e '.errors' <<<"$relaxed" >/dev/null; then
echo "Unexpected GraphQL errors" >&2
exit 1
fi// The same update with SKIP_UNCHANGED_FIELDS: only the city is validated.
const relaxed = await gql(UPDATE, { input: update, validationLevel: ["SKIP_UNCHANGED_FIELDS"] });
console.log("SKIP_UNCHANGED_FIELDS:", JSON.stringify(relaxed, null, 2));
if (relaxed.errors) throw new Error(`Unexpected GraphQL errors: ${JSON.stringify(relaxed.errors)}`);# The same update with SKIP_UNCHANGED_FIELDS: only the city is validated.
relaxed = gql(UPDATE, {"input": update, "validationLevel": ["SKIP_UNCHANGED_FIELDS"]})
print("SKIP_UNCHANGED_FIELDS:", json.dumps(relaxed, indent=2))
if relaxed.get("errors"):
sys.exit(f"Unexpected GraphQL errors: {json.dumps(relaxed['errors'])}"){
"success": true,
"data": [
"0a000000-0000-4000-a000-000000000001"
]
}{
"data": {
"bulkUpdateAccount": {
"result": {
"updated": [
"0a000000-0000-4000-a000-000000000001"
],
"errors": []
}
}
}
}2 is the narrow answer for this case; SKIP_ALL turns off far more than you need.
Limits
| Limit | Details |
|---|---|
| Where it applies | Create, update and batch-modify; refused on batch-delete and batch-restore (HTTP 400, code 118) |
| Never skipped | A missing name on create |
| Refused values | Two internal bits in any form, -1 included: HTTP 400, code 117 ERROR_ |