sveltekit-superforms: Nested objects defaults not working/Nested Errors are flattened into an array of strings

Thanks for the great module, I’m a bit new so not sure if this is a superforms issue or a zod issue but, I have the following types:

If there are validation issues on the ListingProject (which is a nested zod object), the errors show up as [error1, error2, error3] so in the form it’s a bit difficult to show the error on the nested object.

Plus I assumed that superforms assumes the defaults based on the object type, for the ListingProject object, there’s a couple of fields that are arrays of strings or objects, when trying to build forms that use these fields you get "cannot read attribute of undefined (reading length).

so what I do is in the nested object definition I put .default({ array1: [], array2: []} is this the best way to go about it?

Thanks for the great work!

import { z } from 'zod';

export const SocialNetworks = z.enum([
	'twitter',
	'discord',
	'telegram',
	'facebook',
	'instagram',
	'others'
]);

const SocialData = z.object({
	network: SocialNetworks,
	handle: z.string().min(1).max(100),
	members: z.number().min(0)
});

export type ListingType = z.infer<typeof Listing>;

export const ProjectTypes = ['NFT', 'DeFi', 'DAO', 'DEX', 'Games', 'Others'];

export const ListingProject = z.object({
	name: z.string().min(1).max(100),
	helloMoonCollectionId: z
		.string()
		.min(1)
		.max(50)
		// .optional()
		.default('040de757c0d2b75dcee999ddd47689c4'),
	collectionName: z.string().min(1).max(100),
	launchDate: z.date(),
	launchPrice: z.number().min(0),
	socials: z.record(SocialData).default({}),
	links: z.record(z.string().url()),
	floorPriceAtListing: z.number().min(0),
	listingsAtListing: z.number().min(0),
	volumeAtListing: z.number().min(0),
	ownersAtListing: z.number().min(0),
	supply: z.number().min(1),
	description: z.string().min(1).max(500),
	image: z.string().url(),
	website: z.string().url(),
	categories: z.array(z.enum(ProjectTypes)).default([]),
	network: z
		.enum(['Solana', 'Ethereum', 'Binance Smart Chain', 'Polygon', 'Solana', 'Others'])
		.default('Solana')
});

export const ProjectAddressTypes = z.nativeEnum({
	royalty: 'Royalty',
	treasury: 'Treasury',
	creatorAddress: 'Creator Address',
	updateAuthority: 'Update Authority',
	mintAuthority: 'Mint Authority',
	freezeAuthority: 'Freeze Authority',
	claimAuthority: 'Claim Authority',
	others: 'Others'
});

export const ProjectAddress = z.object({
	network: z.enum(['Solana', 'Ethereum', 'Binance Smart Chain', 'Polygon', 'Solana', 'Others']),
	address: z.string().min(1).max(100),
	balance: z.number().min(0).optional(),
	type: ProjectAddressTypes,
	note: z.string().min(1).max(100)
});

const ProjectFinancials = z.object({
	// item
	item: z.string().min(1).max(100),
	// item value
	value: z.number().min(0),
	// item value unit
	unit: z.enum(['SOL', 'USD', 'ETH', 'BNB', 'MATIC', 'others']).default('SOL')
});

export const TechStackItem = z.object({
	name: z.string().min(1).max(100),
	description: z.string().min(1).max(1000),
	type: z.enum(['backend', 'frontend', 'database', 'others']),
	url: z.string().url(),
	technology: z.string().min(1).max(100),
	version: z.string().min(1).max(100).default('latest')
});

export enum ListingPurpose {
	takeoverFull = 'takeoverFull',
	takeoverPartial = 'takeoverPartial',
	funding = 'funding',
	others = 'others'
}

export const PricingUnits = ['SOL', 'USD', 'ETH', 'BNB', 'MATIC'];

export const ListingPurposes = z.enum(['takeoverFull', 'takeoverPartial', 'funding', 'others']);

export const Listing = z.object({
	id: z.string().uuid(),
	user: z.string().min(1).max(50),
	created: z.date().default(() => new Date()),
	updated: z.date().nullable(),
	financials: ProjectFinancials,
	techStack: z.array(TechStackItem),
	project: ListingProject.default({ socials: {}, network: 'Solana', categories: [] }),
	headline: z.string().min(1).max(100),
	reason: z.string().min(1).max(1000),
	// purpose options ['takeover-full', 'takeover-partial', 'funding', 'others'] array
	purpose: z.array(ListingPurposes),
	partialTakeoverPercentage: z.number().min(0).max(100).optional(),
	overview: z.string().min(1).max(1000),
	addresses: z.array(ProjectAddress),
	price: z.number(),

	priceUnit: z.enum(PricingUnits).default('SOL'),
	priceIncludes: z.array(z.string().min(1).max(100)),
	type: z.enum(['auction', 'fixedPrice', 'others']).default('fixedPrice')
});

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 29 (17 by maintainers)

Most upvoted comments

@teenjuna and @Bewinxed maybe you can give some feedback on the nested error structure I have in mind for 0.6.0. It’s a slight rewrite of the Zod structure:

Given this schema:

const schema = z.object({
  id: z.number().positive(),
  users: z
    .object({
      name: z.string().min(2).regex(/X/),
      posts: z
        .object({ subject: z.string().min(1) })
        .array()
       .min(2)
        .optional()
    })
    .array()
});

An error structure like this can be the result:

{
  id: ['Required'],
  users: {
    '0': {
      name: ['String must contain at least 2 character(s)', 'Invalid'],
      posts: {
        '0': { subject: ['String must contain at least 1 character(s)'] },
        _errors: ['Array must contain at least 2 element(s)']
      }
    }
  }
}

This means that the top-level error handling is still the same, and if you have a nested object, you’re sure to be aware of that, and have to use the other syntax. And maybe the same can be done with constraints. What do you think?

Ouch! I hope the Zod inference can be of help to avoid that level of complication…

Yeah, supporting this with a good TS experience will result in some type witchcraft. Just looks at the code of felte: https://github.com/pablo-abc/felte/blob/main/packages/common/src/types.ts