|
CREATE OR REPLACE |
|
FUNCTION VALIDATE_SPECIFICATION(JSONB, JSONB) |
|
RETURNS BOOLEAN |
|
LANGUAGE PLPGSQL |
|
AS $PGSQL$ |
|
DECLARE |
|
-- extra keys (first use), required keys (second use) |
|
keys TEXT[]; |
|
|
|
-- iteration variable for checking each existing field |
|
spec RECORD; |
|
|
|
-- the return state, i.e., whether the object passes the specification |
|
ret BOOLEAN := TRUE; |
|
BEGIN |
|
-- find any keys which are not covered by the spec |
|
SELECT ARRAY_AGG(jsonb_object_keys) INTO keys |
|
FROM |
|
JSONB_OBJECT_KEYS($1) LEFT JOIN |
|
JSONB_TO_RECORDSET($2) AS ("key" TEXT, t TEXT, r BOOLEAN) ON |
|
jsonb_object_keys = jsonb_to_recordset.key |
|
WHERE jsonb_to_recordset.key IS NULL; |
|
|
|
-- if we got any keys from the last query, then we've got extra keys |
|
IF ARRAY_LENGTH(keys, 1) > 0 THEN |
|
RAISE WARNING 'Extra key(s): %', ARRAY_TO_STRING(keys, ', ') |
|
USING ERRCODE = 'datatype_mismatch'; |
|
|
|
-- this is a failure condition |
|
ret := FALSE; |
|
END IF; |
|
|
|
-- collect object keys for quick reference, reusing the 'keys' var |
|
SELECT ARRAY_AGG(jsonb_object_keys) INTO keys |
|
FROM JSONB_OBJECT_KEYS($1); |
|
|
|
-- iterate over the specification entries |
|
FOR spec IN |
|
SELECT * |
|
FROM JSONB_TO_RECORDSET($2) AS ("key" TEXT, "type" TEXT, required BOOLEAN) |
|
LOOP |
|
-- if the key declared by the spec is not contained in the object |
|
IF NOT (ARRAY[spec.key] <@ keys) THEN |
|
-- it's no big deal if the spec says it's not required |
|
CONTINUE WHEN NOT spec.required; |
|
|
|
-- otherwise make sure to tell the user why this will fail |
|
RAISE WARNING 'Missing required key "%"', spec.key |
|
USING ERRCODE = 'datatype_mismatch'; |
|
|
|
-- and make sure we'll return that failure state |
|
ret := FALSE; |
|
END IF; |
|
|
|
-- check to make sure the JSON type of the given entry is correct |
|
IF JSONB_TYPEOF($1 #> ARRAY[spec.key]) <> spec.type THEN |
|
RAISE WARNING |
|
'Bad value ("%" is type `%`) for key "%"', |
|
$1 #>> ARRAY[spec.key], |
|
JSONB_TYPEOF($1 #> ARRAY[spec.key]), |
|
spec.key |
|
USING |
|
ERRCODE = 'datatype_mismatch', |
|
HINT = 'The proper datatype is `' || spec.type || '`'; |
|
|
|
-- mismatched types are a failure condition |
|
ret := FALSE; |
|
END IF; |
|
END LOOP; |
|
|
|
-- give the calling function our final state |
|
RETURN ret; |
|
END; |
|
$PGSQL$; |
|
|
|
COMMENT ON FUNCTION VALIDATE_SPECIFICATION(JSONB, JSONB) IS $MARKDOWN$ |
|
Generic JSONB Structure Validation |
|
================================== |
|
|
|
The first argument to this function is the JSONB element to be validated. The |
|
second argument is the structure used to validate it, as a JSONB array of |
|
objects. Though the second argument is not validated (to make it possible to |
|
send in extended objects), if each object does not have a minimum of the |
|
following keys, the behavior is undocumented (but the function will likely |
|
blow up): |
|
|
|
* `key` - A field name that should be present in the function. It must be a |
|
string. |
|
* `type` - A valid JSONB subtype. See the [docs][] for `jsonb_typeof()` for |
|
valid candidate values for this field. |
|
* `required` - A boolean value indicating whether or not the `key` **must** |
|
be present in the object being validated. If it is false, the `key` is |
|
optional. |
|
|
|
Note that, though some fields are *required*, any fields that are not listed |
|
**must** be *absent*, or else a warning will be raised and the object will be |
|
considered not to match. |
|
|
|
Diagnostics |
|
----------- |
|
|
|
* 'Extra keys: x, y, ...' - When a given specification row does not describe |
|
all of the elements in the object being validated. |
|
* 'Missing required key "x"' - When a given specification row has `required` |
|
set to `true`, but the object being validated does not contain that key. |
|
* 'Bad value ("x" is type y) for key "z"' - When a given object doesn't |
|
match the appropriate target datatype for a given key. |
|
- The 'HINT' value will clarify the requested type. |
|
|
|
Examples |
|
-------- |
|
|
|
### Valid object: |
|
|
|
SELECT VALIDATE_SPECIFICATION( |
|
'{"example": "correct"}', |
|
'[ |
|
{"key": "example", "type": "string", "required": true} |
|
{"key": "optional", "type": "number", "required": false} |
|
]' |
|
); |
|
|
|
=> 't' |
|
|
|
### Invalid object, missing required key "oops": |
|
|
|
SELECT VALIDATE_SPECIFICATION( |
|
'{"example": "incorrect"}', |
|
'[ |
|
{"key": "example", "type": "string", "required": true}, |
|
{"key": "oops", "type": "string", "required": true} |
|
]' |
|
); |
|
|
|
WARNING: Missing required key "oops" |
|
=> 'f' |
|
|
|
### Invalid object, extra keys "oops" and "bad": |
|
|
|
SELECT VALIDATE_SPECIFICATION( |
|
'{"example": "incorrect", "oops": 1, "bad": true}', |
|
'[{"key": "example", "type": "string", "required": true}]' |
|
); |
|
|
|
WARNING: Extra key(s): oops, bad |
|
=> 'f' |
|
|
|
### Invalid object, bad datatype on "bad" key: |
|
|
|
SELECT VALIDATE_SPECIFICATION( |
|
'{"example": "incorrect", "bad": "fifty"}', |
|
'[ |
|
{"key": "example", "type": "string", "required": true}, |
|
{"key": "bad", "type": "number", "required": true}, |
|
]' |
|
); |
|
|
|
WARNING: Bad value ("fifty" is type `string`) for key "bad" |
|
HINT: The proper datatype is `number` |
|
=> 'f' |
|
|
|
----- |
|
|
|
docs: http://www.postgresql.org/docs/9.4/static/functions-json.html#FUNCTIONS-JSON-PROCESSING-TABLE |
|
$MARKDOWN$; |