astrolabe-apps avatar

astrolabe-schemas

C# schema definition system bridging .NET types to TypeScript/React form definitions. Use when gener

作者 astrolabe-apps|オープンソース

Astrolabe.Schemas - Schema Definition System

Overview

Astrolabe.Schemas is a C# library for defining schemas that bridge .NET types to TypeScript/React form definitions. It enables schema-driven development where UI components are automatically generated from type-safe schema definitions.

When to use: Use this library when you need to generate TypeScript schema definitions from C# types, create form schemas for @react-typed-forms/schemas, or implement schema-driven UI generation.

Package: Astrolabe.Schemas Dependencies: Astrolabe.Common, System.Text.Json TypeScript Counterpart: @react-typed-forms/schemas Target Framework: .NET 7-8

Key Concepts

1. Schema-Driven Development

The core pattern is to define schemas in C# that get serialized to JSON and consumed by TypeScript/React components. This ensures type safety across the full stack.

2. Field Types

Schemas support these core field types that map to JSON:

  • String: JSON string
  • Bool: JSON boolean
  • Int: JSON number
  • Double: JSON number
  • Date: JSON string (yyyy-MM-dd format)
  • DateTime: JSON string (ISO8601 format)
  • Compound: JSON object with nested fields
  • Collection: JSON array of any field type

3. Schema Fields

A SchemaField describes a single property including:

  • Field name and display name
  • Type information
  • Validation rules (required, min/max, patterns)
  • Default values
  • Field options (for enums/dropdowns)
  • Visibility and edit rules

4. Form Schemas

Complete form schemas define the structure of data entry forms, combining multiple fields with layout and validation rules.

Common Patterns

Defining Schema Fields

using Astrolabe.Schemas;

// String field
var nameField = new SimpleSchemaField(FieldType.String.ToString(), "name")
{
    DisplayName = "Full Name",
    Required = true
};

// Integer field with default
var ageField = new SimpleSchemaField(FieldType.Int.ToString(), "age")
{
    DisplayName = "Age",
    DefaultValue = 18
};

// Date field
var birthDateField = new SimpleSchemaField(FieldType.Date.ToString(), "birthDate")
{
    DisplayName = "Date of Birth"
};

// Boolean field
var activeField = new SimpleSchemaField(FieldType.Bool.ToString(), "isActive")
{
    DisplayName = "Active",
    DefaultValue = true
};

// Enum field with options
var statusField = new SimpleSchemaField(FieldType.String.ToString(), "status")
{
    DisplayName = "Status",
    Options = new[]
    {
        new FieldOption("Active", "active"),
        new FieldOption("Inactive", "inactive"),
        new FieldOption("Pending", "pending")
    }
};

Building Schemas for C# Classes

using Astrolabe.Schemas;

// Define your model
public class UserProfile
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public int Age { get; set; }
    public DateTime DateOfBirth { get; set; }
    public UserRole Role { get; set; }
}

public enum UserRole
{
    Admin,
    User,
    Guest
}

// Build schema manually
var userProfileSchema = new SchemaField[]
{
    new SimpleSchemaField(FieldType.String.ToString(), "firstName")
    {
        DisplayName = "First Name",
        Required = true
    },
    new SimpleSchemaField(FieldType.String.ToString(), "lastName")
    {
        DisplayName = "Last Name",
        Required = true
    },
    new SimpleSchemaField(FieldType.String.ToString(), "email")
    {
        DisplayName = "Email",
        Required = true,
        Validators = new[]
        {
            new SchemaValidator
            {
                Type = "pattern",
                Value = @"^[^\s@]+@[^\s@]+\.[^\s@]+$",
                Message = "Please enter a valid email"
            }
        }
    },
    new SimpleSchemaField(FieldType.Int.ToString(), "age")
    {
        DisplayName = "Age",
        DefaultValue = 18
    },
    new SimpleSchemaField(FieldType.Date.ToString(), "dateOfBirth")
    {
        DisplayName = "Date of Birth"
    },
    new SimpleSchemaField(FieldType.String.ToString(), "role")
    {
        DisplayName = "Role",
        Options = new[]
        {
            new FieldOption("Administrator", "Admin"),
            new FieldOption("User", "User"),
            new FieldOption("Guest", "Guest")
        }
    }
};

Compound Fields (Nested Objects)

using Astrolabe.Schemas;

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
}

public class Person
{
    public string Name { get; set; }
    public Address HomeAddress { get; set; }
}

// Build schema with nested object
var personSchema = new SchemaField[]
{
    new SimpleSchemaField(FieldType.String.ToString(), "name")
    {
        DisplayName = "Name",
        Required = true
    },
    new CompoundField(
        "homeAddress",
        new SchemaField[]
        {
            new SimpleSchemaField(FieldType.String.ToString(), "street")
            {
                DisplayName = "Street"
            },
            new SimpleSchemaField(FieldType.String.ToString(), "city")
            {
                DisplayName = "City"
            },
            new SimpleSchemaField(FieldType.String.ToString(), "country")
            {
                DisplayName = "Country"
            }
        },
        TreeChildren: false
    )
    {
        DisplayName = "Home Address"
    }
};

Validation Rules

using Astrolabe.Schemas;

// Required field
var requiredField = new SimpleSchemaField(FieldType.String.ToString(), "email")
{
    DisplayName = "Email",
    Required = true
};

// Pattern (regex) validation
var phoneField = new SimpleSchemaField(FieldType.String.ToString(), "phone")
{
    DisplayName = "Phone Number",
    Validators = new[]
    {
        new SchemaValidator
        {
            Type = "pattern",
            Value = @"^\d{3}-\d{3}-\d{4}$",
            Message = "Please enter a valid phone number (e.g., 555-555-5555)"
        }
    }
};

// Multiple validators with custom error messages
var emailField = new SimpleSchemaField(FieldType.String.ToString(), "email")
{
    DisplayName = "Email",
    Required = true,
    RequiredText = "Email is required",
    Validators = new[]
    {
        new SchemaValidator
        {
            Type = "pattern",
            Value = @"^[^\s@]+@[^\s@]+\.[^\s@]+$",
            Message = "Please enter a valid email address"
        }
    }
};

Generating TypeScript Schemas from C# Types

Schemas are not built by hand from SimpleSchemaField for TypeScript export — that pattern is for runtime use only. To emit .ts files consumable by @react-typed-forms/schemas, use SchemaFieldsGenerator from Astrolabe.Schemas.CodeGen.

using Astrolabe.CodeGen.Typescript;
using Astrolabe.Schemas;
using Astrolabe.Schemas.CodeGen;

// Emit a .ts module with `buildSchema<T>(...)` calls for each registered type.
var gen = new SchemaFieldsGenerator(new SchemaFieldsGeneratorOptions("./client"));
var allGenSchemas = gen
    .CollectDataForTypes(typeof(UserProfile), typeof(OtherForm))
    .ToList();
var file = TsFile.FromDeclarations(
    GeneratedSchema.ToDeclarations(allGenSchemas, "SchemaMap").ToList()
);
var sourceText = file.ToSource(); // write to schemas.ts

The companion API is FormBuilder<TConfig>.Form<TSchema>(value, name, config) and FormDefinition.GenerateFormModule(...) — these emit a TypeScript module that ties each schema to a ControlDefinition[] JSON. See the appforms-bootstrap skill for the full controller and codegen flow.

Best Practices

1. Use Proper Field Construction

// ✅ DO - Use object initializers for clarity
var schema = new SimpleSchemaField(FieldType.String.ToString(), "propertyName")
{
    DisplayName = "Display Name",
    Required = true
};

// ⚠️ CAUTION - Be careful with field names (must match TypeScript/JSON property names)
var schema = new SimpleSchemaField(FieldType.String.ToString(), "propertyName") // camelCase
{
    DisplayName = "Property Name"
};

2. Create Options from Enums

// ✅ DO - Create options from enum values
public enum Status { Active, Inactive, Pending }

var statusField = new SimpleSchemaField(FieldType.String.ToString(), "status")
{
    DisplayName = "Status",
    Options = Enum.GetValues<Status>()
        .Select(s => new FieldOption(s.ToString(), s.ToString()))
        .ToArray()
};

3. Keep Display Names User-Friendly

// ✅ DO - Use clear, user-friendly display names
new SimpleSchemaField(FieldType.String.ToString(), "firstName")
{
    DisplayName = "First Name"
};

// ❌ DON'T - Use technical names
new SimpleSchemaField(FieldType.String.ToString(), "firstName")
{
    DisplayName = "firstName" // Not user-friendly
};

Troubleshooting

Common Issues

Issue: Schema not generating options for enum

  • Cause: Enum type not properly recognized
  • Solution: Ensure the enum is a standard C# enum type and properly referenced in the schema builder

Issue: TypeScript can't import schema JSON

  • Cause: Schema not generated through SchemaFieldsGenerator / GeneratedSchema.ToDeclarations
  • Solution: Don't hand-serialize SchemaField records — emit a .ts module via the CodeGen pipeline so the output uses the buildSchema/makeScalarField helpers from @react-typed-forms/schemas.

Issue: Nested objects not rendering correctly

  • Cause: Compound field not properly defined
  • Solution: Use CompoundField (with a Children collection) for nested objects, not SimpleSchemaField.

Project Structure Location

  • Path: Astrolabe.Schemas/
  • Project File: Astrolabe.Schemas.csproj
  • Namespace: Astrolabe.Schemas
astrolabe-schemas - Claude Code・Cursor 対応の AIエージェント Skill | Agent Skills