New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@standard-schema/spec

Package Overview
Dependencies
Maintainers
0
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@standard-schema/spec

A standard interface for TypeScript schema validation libraries

  • 1.0.0-beta.0
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
403K
increased by277.8%
Maintainers
0
Weekly downloads
 
Created
Source

🦆
Standard Schema

A proposal for a common standard interface for TypeScript and JavaScript schema validation libraries.


This is a proposal for a standard interface to be adopted across TypeScript validation libraries. The goal is to make it easier for open-source libraries to accept user-defined schemas as part of their API, in a library-agnostic way.Type safety is important, but it doesn't make sense for every API library and framework to implement their own runtime type validation system. This proposal establishes a common pattern for exchanging type validators between libraries.

The standard interface

export interface StandardSchema<Input = unknown, Output = Input> {
  '~standard': number; // version number
  '~types'?: {
    input: Input;
    output: Output;
  };
  '~vendor': string; // optional vendor name
  
  // this must be implemented at runtime 
  // does not need to exist in the type signature
  '~validate': (
    input: { value: unknown },
    ...args: any[] 
  ) => StandardResult<Output> | Promise<StandardResult<Output>>;
}
export type StandardResult<Value> =
  | StandardSuccessResult<Value>
  | StandardFailureResult;

export interface StandardSuccessResult<Value> {
  value: Value;
  issues?: ReadonlyArray<StandardIssue>;
}

export interface StandardFailureResult {
  value?: unknown;
  issues: ReadonlyArray<StandardIssue>;
}

export interface StandardIssue {
  message: string;
  path?: ReadonlyArray<PropertyKey | { key: PropertyKey }>;
}

Accepting user-defined schemas

So, you're building a library and want to accept user-defined schemas. Great!

First, install @standard-schema/spec as a dev dependency. This package only contains types!

pnpm add --dev @standard-schema/spec

To accept a user-defined schema in your API, use a generic function parameter that extends StandardSchema.

import type { v1 } from '@standard-schema/spec';

// example usage in libraries
function inferSchema<T extends v1.StandardSchema>(schema: T): T {
  return schema;
}

Here's a complete example of how to validate data with a user-provided schema.

// example usage in libraries
import { StandardSchema, InferOutput } from '@standard-schema/spec';
import { CoolSchema } from 'some-cool-schema-library'; // not a real library

// example usage in libraries
function acceptSchema<T extends StandardSchema>(schema: T): T   {
  // currently only the first version exists
  if(schema["~standard"] === 1){
    return schema as T;
  }
  throw new Error(`Unrecognized Standard Schema version: ${schema["~standard"]}`);
}

Using the generic acceptSchema function, you can now accept user-defined schemas in your API.

const someSchema = new CoolSchema<{ name: string }>();

// 1. accept a user-defined schema via your API
const inputSchema = acceptSchema(someSchema); 

// 2. use the schema to validate data
const value = { name: 'Billie' };
const result = inputSchema['~validate']({ value });

if (result.issues) {
  result.issues; // readonly StandardIssue[]
} else {
  result.value; // unknown
}

// 3. infer input and output types
type Output = InferOutput<typeof inputSchema>; // { name: string }
type Input = InferInput<typeof inputSchema>; // { name: string }

Note the await after calling ~validate. This is because the method can return a Promise. You should handle Promises however you see fit in your library.

Implementing the standard: schema library authors

To make your library compatible with the Standard Schema spec, your library must be compatible in both the static and the runtime domain.

Static domain

Your schemas should conform to the following interface. This is all that's required in the static domain to be compatible with the Standard Schema spec.

interface BaseSchema {
  // which version of Standard Schema this schema is compatible with
  '~standard': number;
  "~vendor": string; // the name of your library
  '~types'?: { output: unknown; input: unknown };
}

Runtime domain

The Standard Schema spec has built-in versioning. Successive versions of the spec may require different methods or properties to be defined on your schema at runtime. At the moment there is only one version: 1.

To be compatible with Standard Schema v1, your schema needs to implement a single method: ~validate. The signature is already described above.

You can hide this method/function from the public-facing type signature if you like, but it isn't necessary. The easiest way to do this is with the private modifier (if you're using class definitions).

abstract class CoolSchema<T> implements v1.StandardSchema<T, T> {
  "~standard": 1; // numeric literal `1`
  "~vendor": "some-cool-schema-library"; // the name of your library
  "~types":{
    input: T;
    output: T;
  };
  
  abstract "~validate"(input: {value: unknown}): v1.StandardOutput<T>;
}

class StringSchema extends CoolSchema<string> {
  override "~validate"(input: {value: unknown}): v1.StandardOutput<string> {
    if(typeof input.value === 'string'){
      return {value: input.value};
    }
    return {issues: [{message: 'not a string'}]};
  }
}

FAQ

Do I need to include @standard-schema/spec as a dependency?

You can include @standard-schema/spec as a dev dependency and consume the library exclusively with import type. The @standard-schema/spec package only exports types. You can also just copy-paste the types into your project.

Why tilde ~?

The goal of prefixing the key names with ~ is to both avoid conflicts with existing API surface and to de-prioritize these keys in auto-complete. The ~ character is one of the few ASCII characters that occurs after A-Za-z0-9 lexicographically, so VS Code puts these suggestions at the bottom of the list.

Screenshot 2024-04-10 at 5 48 30 PM

Why not use symbols for the keys?

In TypeScript, using a plain Symbol inline as a key always collapses to a simple symbol type. This would cause conflicts with other schema properties that use symbols.

const object = {
  [Symbol.for('~output')]: 'some data',
};
// { [k: symbol]: string }

By contrast, declaring the symbol externally makes it "nominally typed". This means the key is sorted in autocomplete under the variable name (e.g. testSymbol below). Thus, these symbol keys don't get sorted to the bottom of the autocomplete list, unlike ~-prefixed string keys.

Screenshot 2024-04-10 at 9 33 33 PM

Keywords

FAQs

Package last updated on 27 Sep 2024

Did you know?

Socket

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.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc