Skip to main content
Wildo.ai Coming soon

Declaring a resource

Give each variation the fields it needs

Share the common fields of a business object while defining what changes for each variation.

One-off and recurring tasks share a common definition, with a weekly rule on the recurring variation.

Give each variation the fields it needs

An individual customer and a company share contact information, but only the company needs a registration number. A recurring task shares its title with an ordinary task, but also needs a recurrence rule.

A schema family keeps those common fields together and describes the additions for each variation. One field identifies which shape applies, so the application can work with a precise definition instead of a long list of fields that might or might not belong.

You choose the variations that matter to the product. They remain part of the same resource, with shared relationships and operations.

Example — One task model, two kinds of task

A one-time task has a title and a due date. A recurring task adds its recurrence interval and optional start and end dates. Both appear as tasks, while the recurring version carries the information needed to repeat.

For engineers

Keep shared fields in the base

Declare the discriminator on the base schema. Wonder Todos uses recurringType, a named enum field marked isDiscriminator(). The base contains the title, priority and other fields shared by both kinds of task. The recurring extension contributes only the recurrence settings.

These two declarations come from todos.schemas.ts; the intervening explanatory comment is omitted:

const todosRecurringExtension = z.object({
  recurrence: z.enum([Todos_Recurrence.DAILY, Todos_Recurrence.WEEKLY, Todos_Recurrence.MONTHLY]),
  startDate: z.date().optional(),
  endsDate: z.date().optional(),
});
export const TodosSchemaFamily = createSchemaFamily({
  base: Todos_BaseSchema,
  discriminator: {
    field: 'recurringType',
    baseValues: [Todos_RecurrenceType.ONE_TIME],
  },
  variants: {
    [Todos_RecurrenceType.RECURRING]: {
      extend: todosRecurringExtension,
    },
  },
});

baseValues assigns the one-time value to the base shape. The RECURRING entry extends that base with todosRecurringExtension, so its required recurrence field belongs to the recurring task. The optional dates keep their own optionality; choosing a variant does not make every added field required.

Use the family’s outputs together

createSchemaFamily returns the extended variant schemas, a discriminated union and the inheritance definition consumed by resource configuration. Wonder Todos exports the base and recurring schemas for callers and passes the family’s inheritance configuration to its resource factory. This connects the variant model to derived operation contracts rather than maintaining separate resource declarations.

The outputs have different jobs. In todos.schemas.ts, the resource’s common schema remains the base; the recurring schema is available separately:

export const Todos_Schema = Todos_BaseSchema;

export const TodosRecurringSchema = TodosSchemaFamily.variants[Todos_RecurrenceType.RECURRING];

The same application’s createResourceConfiguration_Initialization(...) call binds both the base and the inheritance definition. These are selected properties from the full factory configuration:

mainSchema: Todos_Schema,
resourceIdentifier: TasksManager_ResourceType.TODOS,
resourceFieldIdentifier: TasksManager_ResourceFieldIdentifier[TasksManager_ResourceType.TODOS],
resourceRelationships: resourcesRelationships,
inheritenceSchemaDefinition: TodosSchemaFamily.inheritenceSchemaDefinition,

Do not substitute TodosSchemaFamily.union for mainSchema in this pattern. The union validates complete family values; the base plus inheritance definition lets resource consumers derive the contracts they need. createSchemaFamily and createResourceConfiguration_Initialization are public exports of @wildo-ai/saas-models.

Check what each shape accepts

This example validates complete model values, not HTTP CREATE payloads. It supplies the model’s required identifiers; HTTP creation derives a different request contract and assigns the stored identity through the resource service.

const common = {
  _id: 'example-todo',
  organizationId: 'example-org',
  todoListId: 'example-list',
  title: 'Prepare the launch',
};

// Accepted: the one-time shape does not require recurrence.
TodosSchemaFamily.union.safeParse({
  ...common,
  recurringType: Todos_RecurrenceType.ONE_TIME,
});

// Rejected: choosing RECURRING also requires its recurrence field.
TodosSchemaFamily.union.safeParse({
  ...common,
  recurringType: Todos_RecurrenceType.RECURRING,
});

// Accepted: the weekly rule completes the recurring shape.
TodosSchemaFamily.union.safeParse({
  ...common,
  recurringType: Todos_RecurrenceType.RECURRING,
  recurrence: Todos_Recurrence.WEEKLY,
});

Inspect safeParse(...).success before using its data. The second result is unsuccessful with an issue at recurrence; the first and third succeed. The family union also rejects an unknown discriminator value. Optional startDate and endsDate remain optional on the recurring variant. This validates the declared shapes; resource authorization, stored references and operation-specific input rules remain separate checks.

The builder checks coverage of an enum discriminator and detects values claimed by both the base and a variant. Keep the discriminator as a named enum when the possible shapes form a known set. A plain string does not provide the same finite list to check.

Carry the distinction into the product

Choose which controls and sections each variation needs in UI behavior, and document the meaning of its added fields in the resource specification. An existing field that moves into a variant changes the accepted shape, so review the relevant create and update contracts and existing stored records together.

The runtime resolver selects the schema using the record’s discriminator. Its fallback for an unrecognized value is the base schema, which makes valid discriminator values important at the boundary where records enter the application.

Building a B2B product or an internal tool?

Wildo is not self-service yet. Tell us what you have in mind and we will say plainly whether it fits, and what happens next.