COCPORN
COCPORN3w ago

Validation of (partial) Convex-schemas

I have a use-case where it would be super helpful to be able to call something like:
const schema = v.object({
"name": v.string()
});

const input = schema.parse(input);
const schema = v.object({
"name": v.string()
});

const input = schema.parse(input);
...similar to the "zod"-functionality parse/safeParse. The reasoning for this is that I will be validating partial data returned from an LLM that will fit into a Convex-schema, and I don't want to maintain two schemas. My problem is that I cannot find the parse/safeParse-methods anywhere, but they "must be there" (right?), as the data is indeed parsed/validated by Convex at some point. Thanks to @ian for the help so far.
15 Replies
Convex Bot
Convex Bot3w ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
ian
ian3w ago
The data is validated internally (in Rust) server-side for the database schema & function args. However, you could pretty easily make a validation function, if you're willing to downgrade v.id to string validation
ian
ian3w ago
GitHub
convex-test/index.ts at main · get-convex/convex-test
Testing harness for pure-JS Convex tests. Contribute to get-convex/convex-test development by creating an account on GitHub.
COCPORN
COCPORNOP3w ago
Yeah, for this usecase I don't need v.id at all, I forgot to mention that. This is partial data that will fix into a larger request/document.
ian
ian3w ago
This is it, but there's a better way ™️ nowadays where you can introspect a validator, vs. working with the JSON
function validateValidator(validator: ValidatorJSON, value: any) {
switch (validator.type) {
case "null": {
if (value !== null) {
throw new Error(`Validator error: Expected \`null\`, got \`${value}\``);
}
return;
}
case "number": {
if (typeof value !== "number") {
throw new Error(
`Validator error: Expected \`number\`, got \`${value}\``,
);
}
return;
}
case "bigint": {
if (typeof value !== "bigint") {
throw new Error(
`Validator error: Expected \`bigint\`, got \`${value}\``,
);
}
return;
}
case "boolean": {
if (typeof value !== "boolean") {
throw new Error(
`Validator error: Expected \`boolean\`, got \`${value}\``,
);
}
return;
}
case "string": {
if (typeof value !== "string") {
throw new Error(
`Validator error: Expected \`string\`, got \`${value}\``,
);
}
return;
}
case "bytes": {
if (!(value instanceof ArrayBuffer)) {
throw new Error(
`Validator error: Expected \`ArrayBuffer\`, got \`${value}\``,
);
}
return;
}
case "any": {
return;
}
case "literal": {
if (value !== validator.value) {
throw new Error(
`Validator error: Expected \`${
validator.value as any
}\`, got \`${value}\``,
);
}
return;
}
case "id": {
if (typeof value !== "string") {
throw new Error(
`Validator error: Expected \`string\`, got \`${value}\``,
);
}
if (tableNameFromId(value) !== validator.tableName) {
throw new Error(
`Validator error: Expected ID for table "${validator.tableName}", got \`${value}\``,
);
}
return;
}
case "array": {
if (!Array.isArray(value)) {
throw new Error(
`Validator error: Expected \`Array\`, got \`${value}\``,
);
}
for (const v of value) {
validateValidator(validator.value, v);
}
return;
}
case "object": {
if (typeof value !== "object") {
throw new Error(
`Validator error: Expected \`object\`, got \`${value}\``,
);
}
if (!isSimpleObject(value)) {
throw new Error(
`Validator error: Expected a plain old JavaScript \`object\`, got \`${value}\``,
);
}
for (const [k, { fieldType, optional }] of Object.entries(
validator.value,
)) {
if (value[k] === undefined) {
if (!optional) {
throw new Error(
`Validator error: Missing required field \`${k}\` in object`,
);
}
} else {
validateValidator(fieldType, value[k]);
}
}
for (const k of Object.keys(value)) {
if (validator.value[k] === undefined) {
throw new Error(
`Validator error: Unexpected field \`${k}\` in object`,
);
}
}
return;
}
}
}
function validateValidator(validator: ValidatorJSON, value: any) {
switch (validator.type) {
case "null": {
if (value !== null) {
throw new Error(`Validator error: Expected \`null\`, got \`${value}\``);
}
return;
}
case "number": {
if (typeof value !== "number") {
throw new Error(
`Validator error: Expected \`number\`, got \`${value}\``,
);
}
return;
}
case "bigint": {
if (typeof value !== "bigint") {
throw new Error(
`Validator error: Expected \`bigint\`, got \`${value}\``,
);
}
return;
}
case "boolean": {
if (typeof value !== "boolean") {
throw new Error(
`Validator error: Expected \`boolean\`, got \`${value}\``,
);
}
return;
}
case "string": {
if (typeof value !== "string") {
throw new Error(
`Validator error: Expected \`string\`, got \`${value}\``,
);
}
return;
}
case "bytes": {
if (!(value instanceof ArrayBuffer)) {
throw new Error(
`Validator error: Expected \`ArrayBuffer\`, got \`${value}\``,
);
}
return;
}
case "any": {
return;
}
case "literal": {
if (value !== validator.value) {
throw new Error(
`Validator error: Expected \`${
validator.value as any
}\`, got \`${value}\``,
);
}
return;
}
case "id": {
if (typeof value !== "string") {
throw new Error(
`Validator error: Expected \`string\`, got \`${value}\``,
);
}
if (tableNameFromId(value) !== validator.tableName) {
throw new Error(
`Validator error: Expected ID for table "${validator.tableName}", got \`${value}\``,
);
}
return;
}
case "array": {
if (!Array.isArray(value)) {
throw new Error(
`Validator error: Expected \`Array\`, got \`${value}\``,
);
}
for (const v of value) {
validateValidator(validator.value, v);
}
return;
}
case "object": {
if (typeof value !== "object") {
throw new Error(
`Validator error: Expected \`object\`, got \`${value}\``,
);
}
if (!isSimpleObject(value)) {
throw new Error(
`Validator error: Expected a plain old JavaScript \`object\`, got \`${value}\``,
);
}
for (const [k, { fieldType, optional }] of Object.entries(
validator.value,
)) {
if (value[k] === undefined) {
if (!optional) {
throw new Error(
`Validator error: Missing required field \`${k}\` in object`,
);
}
} else {
validateValidator(fieldType, value[k]);
}
}
for (const k of Object.keys(value)) {
if (validator.value[k] === undefined) {
throw new Error(
`Validator error: Unexpected field \`${k}\` in object`,
);
}
}
return;
}
}
}
COCPORN
COCPORNOP3w ago
Ah, OK. Will this return the name of the failed field, etc? I fully expect to have to instruct the LLM to do this multiple times, and strong error messages, like the ones returned from the Rust-side, would be helpful.
ian
ian3w ago
Feel free to modify it, but errors look like
Validator error: Missing required field \${k}` in object`,
right now you could pass in the object path as an optional third param so you could get things like "foo.bar.fieldA" instead of just fieldA
COCPORN
COCPORNOP3w ago
Ah, yes, there it is. Hm. It feels like this would be a good part of the core offering, but yes, this is a very nice starting point for me.
ian
ian3w ago
Yeah it's b/c it doesn't transparently work client-side b/c of the ID parsing, but that may change in the future. We didn't want folks to assume it was "just as safe as server-side" in any situations, maybe overly cautious and the ID validation isn't really something any other DBs / ORMs provide, so folks might not expect that anyways
COCPORN
COCPORNOP3w ago
I think it is a fair take. It was just a little confusing for me not knowing the reasoning, being "pretty sure" it "has to be somewhere" when scouring the TypeScript code. 🙂
ian
ian3w ago
totally. and the LLMs are also "pretty sure" they can find it somewhere 🙂
COCPORN
COCPORNOP3w ago
Yeah, those guys need all the help they can get. 😄 But it's good times.
ian
ian3w ago
ok I'm going heads down, but hopefully you have something to get started with
COCPORN
COCPORNOP3w ago
Yes, thanks a lot. Actually, looking at this, it sorta already makes sense to maintain this as a separate zod-schema. For a number of reasons. But thanks for enlightening me on how this is pieced together.
ian
ian3w ago
I just made a validate helper in convex-helpers/validators in convex-helpers@0.1.69-alpha.0 if you want to try it out But yeah if you need more specific validation (email / etc) then zod is a good bet

Did you find this page helpful?