pub fn validate_with(
collection: &str,
record: &Value,
stance: Stance,
) -> Result<(), ShapeError>Expand description
Checks a record against the collection it is being written to.
Four rules, in order.
- The collection must be a syntactically valid NSID —
validate_collection. - A
$typecarried by the record must agree with it. - The record must be representable in the atproto data model.
- The record must satisfy the lexicon that defines the collection, when this server holds one.
The $type check is here because nothing else performs it. jacquard does
not verify $type on deserialize, so a record claiming to be a
record can be written into the memory collection and read back as a
memory, with the lie preserved inside the value. That is a data corruption
no later reader can detect, so it is refused at the only point where both
facts are in hand. It holds for a foreign collection too: $type and the
collection name the same lexicon in atproto, whoever wrote it.
A record with no $type is accepted: the collection already says what it
is, and atproto treats the field as redundant inside a repository.
§The data model check is the one that makes storage faithful
A record is stored as JSON and exported as DAG-CBOR inside a Merkle search
tree. JSON has values the data model does not — a float, most of all — and
under the closed collection set they could only arrive in an undeclared
field, which validation deliberately carries through unchecked. Opening the
collection set makes every field undeclared, so the check moves to the
boundary: a record that cannot be encoded is refused on write rather than
accepted and then breaking every export of the repository afterwards. What
the model can represent survives byte for byte, which is what the
unknown type exists for.
§Why the schema check is conditional
atproto’s Lexicon specification names three stances a service may take: explicit, where every record must resolve to a known schema and satisfy it; optimistic, where a record is validated when a schema is at hand and accepted when it is not; and skip. This server takes the optimistic one, which is the stance a personal data server has to take, because a repository is the account’s and the lexicons are the world’s. Validation is explicit, and that choice held only while the collection set was closed. The set is open now.
use didbot_schema::validate_shape;
use serde_json::json;
// A record under a collection no schema is held for is accepted unchecked.
let foreign = json!({"text": "reading the MST encoder"});
assert!(validate_shape("com.example.thing", &foreign).is_ok());
// The one schema this deployment holds is enforced.
let bad = json!({"did": "did:web:a.example"});
assert!(validate_shape("bot.did.registration", &bad).is_err());