Security News
Input Validation Vulnerabilities Dominate MITRE's 2024 CWE Top 25 List
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
jet-schema
Advanced tools
Simple, zero-dependency, typescript-first schema validation tool, that lets you integrate your own validator-functions.
Most schema validation libraries have fancy functions for validating objects and their properties, but the problem is I usually already have a lot of my own custom validation logic specific for each of my applications (i.e. functions to check primitive-types, regexes for validating strings etc). The only thing that was making me use schema-validation libraries was trying to validate an object. So I thought, why not figure out a way to integrate my all the functions I had already written with something that can validate them against object properties? Well jet-schema does just that :)
If you want a library that includes all kinds of special functions for validating things other than objects, jet-schema is probably not for you. However, the vast majority of projects I've worked on have involved implementing lots of type-checking functions specific to the needs of that project. For example, maybe the email format that's built into the library is different than the one your application needs. Instead of of having to dig into the library's features to validate using your custom method, with jet-schema you can just pass your method.
If you're open to
jet-schema
but think writing your own validator functions could be a hassle, you can copy-n-paste the file (https://github.com/seanpmaxwell/ts-validators/blob/master/src/validators.ts) into your application and add/remove/edit validators as needed.
new
, test
, parse
functions provided automatically on every new schema.transform
wrapper function to modify values before validating them.setDefault
wrapper.Date
constructor can be used to automatically transform and validate any valid date value.ts-node
, unlike typia
).
import { setDefault, transform } from 'jet-schema';
import schema from 'utils/schema';
import { isRelKey, isString, isNumber, isOptionalStr } from 'utils/validators';
interface IUser {
id: number;
name: string;
email: string;
age: number;
created: Date;
address?: {
street: string;
zip: number;
country?: string;
};
}
const User = schema<IUser>({
id: isRelKey,
name: isString,
email: setDefault(isEmail, 'x@x.com'),
age: transform(Number, isNumber),
created: Date,
address: schema<IUser['address']>({
street: isString,
zip: isNumber,
country: isOptionalStr,
}, { optional: true }),
});
npm install -s jet-schema
After installation, you need to configure the schema
function by importing and calling the jetSchema()
function.
jetSchema()
accepts an optional settings object with 3 three options:
defaultValuesMap
: An [["val", "function"]]
nested array specifying which default value should be used for which validator-function: you should use this option for frequently used validator-function/default-value combinations where you don't want to set a default value every time. Upon initialization, the validator-functions will check their defaults. If a value is not optional and you do not supply a default value, then an error will be thrown when the schema is initialized. If you don't set a default value for a validator-function in jetSchema()
, you can also call setDefault
when setting up the schema (the next 3 snippets below contain examples).cloneFn
: A custom clone-function if you don't want to use the built-in function which uses structuredClone
(I like to use lodash.cloneDeep
).onError
: If a validator-function fails then an error is thrown. You can override this behavior by passing a custom error handling function as the third argument. This feature is really useful for testing when you may want to return an error string instead of throw an error.
(property: string, value?: unknown, origMessage?: string, schemaId?: string) => void;
.When setting up jet-schema for the first time, usually what I do is create two files under my
util/
folder:schema.ts
andvalidators.ts
. Inschema.ts
I'll import and call thejet-schema
function then apply any frequently used validator-function/default-value combinations I have and a clone-function. If you don't want to go through this step, you can import theschema
function directly fromjet-schema
.
// "util/validators.ts"
// As mentioned in the intro, you can copy some validators from here (https://github.com/seanpmaxwell/ts-validators/blob/master/src/validators.ts)
export const isStr = (arg: unknown): arg is string => typeof param === 'string';
export const isOptStr = (arg: unknown): arg is string => arg === undefined || typeof param === 'string';
export const isNum = (arg: unknown): arg is string => typeof param === 'number';
⚠️ IMPORTANT: You need to use type-predicates when writing validator-functions. If a value can be
null/undefined
, your validator-function's type-predicate needs account for this (i.e. (arg: unknown): arg is string | undefined => ...
).
// "util/schema.ts"
import jetSchema from 'jet-schema';
import { isNum, isStr } from './validators';
export default jetLogger({
defaultValuesMap: [
[isNum, 0],
[isStr, ''],
],
cloneFn: // pass a custom clone-function here
onError: // pass a custom error-handler here,
});
Now that we have our schema
function setup, let's make a schema. Simply import the schema
function from util/schema.ts
and then your existing validator-functions and pass them as the value to each property in the schema
function. If you want to set a default value at the schema level instead of at the global level in the jetSchema
function, you can import the setDefault
function from jet-schema
and pass it the validator-function and the default value to go with it. Note that setting a default does not modify the validator function in any way. If a property is required then a default must be set for it (either globally or in setDefault
) or else new
won't know what to use as a value if the partial doesn't contain it.
For handling the overall schema's type, there are two ways to go about this, enforcing a schema from a type or infering a type from a schema. I'll show you an example of doing it both ways.
Personally, I like to create an interface first cause I feel like interfaces are great way to document your data-types; however, I created
inferType
because I know some people prefer to setup their schemas first and infer their types from that.
// "models/User.ts"
import { inferType, setDefault } from 'jet-schema';
import schema from 'util/schema.ts';
import { isNum, isStr, isOptionalStr } from 'util/validators.ts';
// **OPTION 1**: Create schema using type
interface IUser {
id: number;
name: string;
email: string;
nickName?: string;
}
const User = schema<IUser>({
id: isNum,
name: isStr,
email: setDefault(isEmail, ''),
nickName: isOptionalStr,
})
// **OPTION 2**: Create type using schema
const User = schema({
id: isNum,
name: isStr,
email: setDefault(isEmail, ''),
nickName: isOptionalStr,
})
const TUser = inferType<typeof User>;
Once you have your custom schema setup, you can call the new
, test
, pick
, and parse
functions. Here is an overview of what each one does:
new
allows you to create new instances of your type using partials. If the property is absent, new
will use the default supplied. If no default is supplied and the property is optional, then the value will be skipped. Runtime validation will still be done on every incoming property. Also, if you pass no parameter then a new instance will be created using all the defaults.test
accepts any unknown value, tests that it's valid, and returns a type-predicate.pick
allows you to select any property and returns an object with the test
and default
functions.parse
is like a combination of new
and test
. It accepts an unknown
value which is not optional, validates the properties but returns a new instance (while removing an extra ones) instead of a type-predicate. If you have an incoming unknown value (i.e. an api call) and you want to validate the properties and return a new instance while removing any possible extra ones, use parse
. Note: only objects will pass the parse
function, even if a schema is nullish, null/undefined
values will not pass.schema
function.new
, test
, pick
, parse
in addition to default
. The value returned from default
could be different from new
if the schema is optional/nullable and the default value is null
or undefined
.schema()
that you can call when using pick
on a child-schema. This can be handy if you need to export a child-schema from one parent-schema to another:interface IUserAlt {
id: number;
address: IUser['address'];
}
const UserAlt = schema<IUserAlt>({
id: isNumber,
address: User.pick('address').schema(),
});
In addition to a schema-object, the schema
function accepts an additional options object parameter. The values here are type-checked against the generic (schema<"The Generic">(...)
) that was passed so you must use the correct values. If your generic is optional/nullable then your are required to pass the object so at runtime the correct values are parsed.
{
optional?: boolean; // default "false", must be true if generic is optional
nullable?: boolean; // default "false", must be true if generic is nullable
init?: boolean | null; // default "true", must be undefined, true, or null if generic is not optional.
nullish?: true; // Use this instead of "{ optional: true, nullable: true; }"
id?: string; // Will identify the schema in error messages
}
The option init
defines the behavior when a schema is a child-schema and is being initialized from the parent. If true (default)
, then a nested child-object will be added to the property when a new instance of the parent is created. However, if a child-schema is optional or nullable, maybe you don't want a nested object and just want it to be null
or skipped entirely. If init
is null
then nullable
must be true
, if false
then optional
must be true
.
In the real world it's very common to have a lot of schemas which are both optional, nullable. So you don't have to write out { optional: true, nullable: true }
over-and-over again, you can write { nullish: true }
as an shorthand alternative.
You can also set the optional id
field, if you need a unique identifier for your schema for whatever reason. If you set this option then it be added to the default error message. This can be useful if you have to debug a bunch of schemas at once (that's pretty much all I use it for).
Here's an example of the options in use:
// models/User.ts
interface IUser {
id: number;
name: string;
address?: { street: string, zip: number } | null;
}
const User = schema<IUser>({
id: isNumber,
name: isString,
address: schema<IUser['address']>({
street: isString,
zip: isNumber,
}, { nullish: true, init: false, id: 'User_address' }),
})
User.new() // => { id: 0, name: '' }
transform()
If you want to modify a value before it passes through a validator-function, you can import the transform
function and wrap your validator function with it. transform
accepts a transforming-function and a validator-function and returns a new validator-function (type-predicate is preserved) which will transform the value before testing it. When calling new
, test
, or parse
, transform
will modify the original object.
If you want to access the transformed value yourself for whatever reason, you can pass a callback as the second argument to the returned validator-function and transform
will supply the modified value to it. I've found transform
can be useful for other parts of my application where I need to modify a value before validating it and then access the transformed value.
import { transform } from 'jet-schema';
const modifyAndTest = transform(JSON.parse, isNumberArray);
let val = '[1,2,3,5]';
console.log(modifyAndTest(val, transVal => val = transVal)); // => true
console.log(val); // => [1,2,3,5] this is number array not a string
For whatever reason, your schema may end up existing in multiple places. If you want to declare part of a schema that will be used elsewhere, you can import the TJetSchema
type and use it to setup one, then merge it with your full schema later.
import schema, { TJetSchema } from 'jet-schema';
const PartOfASchema: TJetSchema<{ id: number, name: string }> = {
id: isNumber,
name: isString,
} as const;
const FullSchema = schema<{ id: number, name: string, e: boolean }>({
...PartOfASchema,
e: isBoolean,
});
console.log(FullSchema.new());
Due to how structural-typing works in typescript, there are some limitations with typesafety that you need to be aware of. To put things in perspective, if type A
has all the properties of type B
, we can use type A
for places where type B
is required, even if A
has additional properties.
Validator functions
If an object property's type can be string | undefined
, then a validator-function whose type-predicate only returns arg is string
will still work. However a if a type predicate returns arg is string | undefined
we cannot use it for type string
. This could cause runtime issues if a you pass a validator function like isString
(when you should have passed isOptionalString
) to a property whose value ends up being undefined
.
interface IUser {
id: string;
name?: string;
}
const User = schema<IUser>({
id: isString, // "isOptionalString" will throw type errors
name: isOptionalString, // "isString" will not throw type errors but will throw runtime errors
})
Child schemas
As mentioned, if a property in a parent is mapped-object type (it as a defined set of keys), then you need to call schema
again for the nested object. If you don't use a generic on the child-schema, typescript will still make sure all the required properties are there; however, because of structural-typing the child could have additional properties. It is highly-recommended that you pass a generic to your child-objects so additional properties don't get added.
interface IUser {
id: number;
address?: { street: string } | null;
}
const User = schema<IUser>({
id: isNumber,
address: schema<IUser['address']>({
street: isString,
// foo: isString, // If we left off the generic <IUser['address']> we could add "foo"
}, { nullish: true }),
});
If you know of a way to enforce typesafety on child-object without requiring a generic please make a pull-request because I couldn't figure out a way.
Date
constructor, jet-schema
sets the type to be a Date
object and automatically converts all valid date values (i.e. string/number
, maybe a Date
object got stringified in an API call) to a Date
object. The default value will be a Date
object with the current datetime.If you need to modify the value of the test
function for a property, (like removing nullables
) then I recommended merging your schema with a new object and adding a wrapper function around that property's test function.
// models/User.ts
import { nonNullable } from 'util/validators.ts';
interface IUser {
id: number;
address?: { street: string, zip: number } | null;
}
const User = schema<IUser>({
id: isNumber,
address: schema<IUser['address']>({
street: isString,
zip: isNumber,
}, { nullish: true }),
})
export default {
// Wrapper function to remove nullables
checkAddr: nonNullable(User.pick('address').test),
...User,
}
When calling the jetSchema
function for this first time, at the very least, I highly recommend you set these default values for each of your basic primitive validator functions, unless of course your application has some other specific need.
// util/schema.ts
import { isNum, isStr, isBool } from 'util/validators.ts';
export default jetLogger({
defaultValuesMap: [
[isNum, 0],
[isStr, ''],
[isBool, false],
],
});
FAQs
Simple, typescript-first schema validation tool
The npm package jet-schema receives a total of 77 weekly downloads. As such, jet-schema popularity was classified as not popular.
We found that jet-schema demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.
Research
Security News
A threat actor's playbook for exploiting the npm ecosystem was exposed on the dark web, detailing how to build a blockchain-powered botnet.