
Research
Malicious npm Packages Impersonate Flashbots SDKs, Targeting Ethereum Wallet Credentials
Four npm packages disguised as cryptographic tools steal developer credentials and send them to attacker-controlled Telegram infrastructure.
@danscan/zod-jsonrpc
Advanced tools
Create JSON-RPC 2.0-compliant clients and servers that use Zod to validate requests and responses.
Create type-safe JSON-RPC API clients and servers in JS/TS using any transport.
@standard-schema/spec
to support Zod, Effect Schema, Valibot, ArkType, and morezod-jsonrpc
uses @standard-schema/spec
for schema validation, supporting any library that implements the Standard Schema specification:
bun add @danscan/zod-jsonrpc
yarn add @danscan/zod-jsonrpc
npm add @danscan/zod-jsonrpc
zod-jsonrpc
makes it easy to create type-safe JSON-RPC servers and clients.
import { createServer, createClient, method } from '@danscan/zod-jsonrpc';
import { z } from 'zod';
// Define methods
const greet = method({
paramsSchema: z.object({ name: z.string() }),
resultSchema: z.string(),
}, ({ name }) => `Hello, ${name}!`);
// Create server and client
const server = createServer({ greet });
const client = createClient({ greet }, async (request) => {
// Your transport layer here: fetch, WebSocket, etc.
return server.request(request);
});
// Make type-safe calls
const greeting = await client.greet({ name: 'World' });
console.log(greeting); // "Hello, World!"
Methods are the core building blocks of your JSON-RPC API. They define the input and output schemas for a given method, and the handler function that will be called when the method is invoked.
const greet = method({
paramsSchema: z.object({ name: z.string() }),
resultSchema: z.string(),
}, ({ name }) => `Hello, ${name}!`);
You can also define methods with a paramsSchema
and a resultSchema
only, and provide a handler function later:
// Import this in your client and server
export const greet = method({
paramsSchema: z.object({ name: z.string() }),
resultSchema: z.string(),
});
// In your server implementation:
import { greet } from './methods';
const server = createServer({
greet: greet.implement(({ name }) => `Hello, ${name}!`),
});
Start by creating a server with your methods:
import { createServer, method, JSONRPCError } from '@danscan/zod-jsonrpc';
import { z } from 'zod';
const server = createServer({
add: method({
paramsSchema: z.object({ a: z.number(), b: z.number() }),
resultSchema: z.number(),
}, ({ a, b }) => a + b),
divide: method({
paramsSchema: z.object({ dividend: z.number(), divisor: z.number() }),
resultSchema: z.number(),
}, ({ dividend, divisor }) => {
if (divisor === 0) {
throw new JSONRPCError.InvalidParams({ message: 'Cannot divide by zero' });
}
return dividend / divisor;
}),
});
The server automatically handles single requests, batch requests, and notifications:
// Single request
const result = await server.request({
id: 1,
method: 'add',
params: { a: 5, b: 3 },
jsonrpc: '2.0',
});
// { id: 1, result: 8, jsonrpc: '2.0' }
// Batch request
const results = await server.request([
{ id: 1, method: 'add', params: { a: 5, b: 3 }, jsonrpc: '2.0' },
{ id: 2, method: 'divide', params: { dividend: 10, divisor: 2 }, jsonrpc: '2.0' },
]);
// [{ id: 1, result: 8, jsonrpc: '2.0' }, { id: 2, result: 5, jsonrpc: '2.0' }]
Servers automatically convert any thrown error into proper JSON-RPC responses:
const server = createServer({
validateAge: method({
paramsSchema: z.object({ age: z.number() }),
resultSchema: z.boolean(),
}, ({ age }) => {
if (age < 0) {
// You can easily construct and throw a JSONRPC-specific error (ParseError, InvalidRequest, MethodNotFound, InvalidParams, InternalError)
throw JSONRPCError.InvalidParams({ message: 'Age cannot be negative', data: { age } });
}
if (age >= 150) {
// Any other kind of thrown error becomes a JSONRPCError.InternalError
throw new Error(`Please don't use Bryan Johnson's age`);
}
return true;
}),
});
Errors are handled individually in batch requests, so one failure doesn't affect others:
const results = await server.request([
{ id: 1, method: 'validateAge', params: { age: 25 }, jsonrpc: '2.0' },
{ id: 2, method: 'validateAge', params: { age: -5 }, jsonrpc: '2.0' },
{ id: 3, method: 'validateAge', params: { age: 200 }, jsonrpc: '2.0' },
]);
// [
// { id: 1, result: true, jsonrpc: '2.0' },
// { id: 2, error: { code: -32602, message: 'Invalid params: Age cannot be negative' }, jsonrpc: '2.0' },
// { id: 3, error: { code: -32603, message: 'Internal error', data: { message: 'Please don\'t use Bryan Johnson\'s age' } }, jsonrpc: '2.0' }
// ]
Build type-safe clients that validate requests and responses:
import { createClient, method } from '@danscan/zod-jsonrpc';
// Define your transport
const sendRequest = async (request) => {
const response = await fetch('/api/jsonrpc', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
});
return response.json();
};
// Create client with method definitions
const client = createClient({
add: method({
paramsSchema: z.object({ a: z.number(), b: z.number() }),
resultSchema: z.number(),
}),
}, sendRequest);
// Make calls with full type safety
const sum = await client.add({ a: 5, b: 3 }); // number
Clients provide a convenient API for batch requests with named results:
const results = await client.batch((ctx) => ({
// Name each call in the batch so you can easily access the results by name
// All calls are executed in parallel, so you can use the results as they come in
sum: ctx.add({ a: 5, b: 3 }),
product: ctx.multiply({ a: 5, b: 3 }),
quotient: ctx.divide({ dividend: 10, divisor: 2 }),
}));
// Handle results with type-safe error checking
if (results.sum.ok) {
console.log('Sum:', results.sum.value); // number
} else {
console.error('Sum failed:', results.sum.error.message);
}
// Process all results
Object.entries(results).forEach(([operation, result]) => {
if (result.ok) {
console.log(`${operation}:`, result.value);
} else {
console.error(`${operation} failed:`, result.error.message);
}
});
Schemas that change your data in any way should usually only be applied once. If you provide a transforming schema in a method on both your client and server, you should use a raw client to avoid double transformation.
// This method is provided to the client and server
const normalize = method({
paramsSchema: z.string().transform(s => s.toUpperCase()),
resultSchema: z.string().transform(s => `Result: ${s}`),
}, (input) => input);
const server = createServer({ normalize });
// If you create your client manually, you can use the `raw` method to create a raw client
// This way, the schema transformation is only applied on the server
const client = createClient({ normalize }, sendRequest);
const rawClient = client.raw();
// When generating a client from a server, it will be a raw client by default
const client = server.createClient(sendRequest);
If you are only using zod-jsonrpc for the client, your schema transformations will only be applied once, so you don't need to use a raw client.
const client = createClient({ normalize }, sendRequest);
const result = await client.normalize('hello'); // "Result: HELLO"
Client has methods that return a new client with different validation modes:
client.raw()
: Skip all schema parsing on the client, delegating to the serverclient.rawParams()
: Skip only parameter validation on the clientclient.rawResults()
: Skip only result validation on the clientclient.validating()
: Re-enable all validation on the clientconst client = createClient({ add }, sendRequest);
// Skip validation of both params and results
const rawClient = client.raw();
const result = await rawClient.add({ a: 5, b: 3 }); // No client-side validation
// Skip only params validation (results still validated)
const rawParamsClient = client.rawParams();
// Skip only result validation (params still validated)
const rawResultsClient = client.rawResults();
// Re-enable params and results validation
const validatingClient = rawClient.validating();
// For example, to disable validation on one request:
client.raw().add({ a: 5, b: 3 });
Integrate with any HTTP framework or transport:
const jsonRpcServer = createServer({ add, divide });
// Bun
Bun.serve({
fetch: async (req) => {
const request = await req.json();
const response = await jsonRpcServer.request(request);
return Response.json(response);
}
});
// Next.js App Router
export async function POST(request: Request) {
const jsonRpcRequest = await request.json();
const jsonRpcResponse = await jsonRpcServer.request(jsonRpcRequest);
return Response.json(jsonRpcResponse);
}
// Express.js
app.post('/jsonrpc', async (req, res) => {
const response = await jsonRpcServer.request(req.body);
res.json(response);
});
For larger applications, separate method definitions from implementations:
src/
api/
methods/ # Shared method definitions
user.ts
auth.ts
server.ts # Server implementation
client.ts # Client setup
Define methods separately to share between client and server:
// api/methods/user.ts
export const getUser = method({
paramsSchema: z.object({ id: z.string().uuid() }),
resultSchema: z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email(),
}),
});
// api/server.ts
import * as methods from './methods';
export const server = createServer({
getUser: methods.getUser.implement(async ({ id }) => {
return await getUserFromDatabase(id);
}),
});
// api/client.ts
import * as methods from './methods';
export const client = createClient({
getUser: methods.getUser,
}, sendRequest);
This structure enables you to share method definitions, maintain type safety across your entire API, and version your API independently of implementation details.
FAQs
Create JSON-RPC 2.0-compliant clients and servers that use Zod to validate requests and responses.
The npm package @danscan/zod-jsonrpc receives a total of 85 weekly downloads. As such, @danscan/zod-jsonrpc popularity was classified as not popular.
We found that @danscan/zod-jsonrpc 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.
Research
Four npm packages disguised as cryptographic tools steal developer credentials and send them to attacker-controlled Telegram infrastructure.
Security News
Ruby maintainers from Bundler and rbenv teams are building rv to bring Python uv's speed and unified tooling approach to Ruby development.
Security News
Following last week’s supply chain attack, Nx published findings on the GitHub Actions exploit and moved npm publishing to Trusted Publishers.