Using Schema References and Custom Types

This document explains how to use schema references to create reusable custom types across your JSON schemas in the platform.
Work with the Reach Team for ImplementationWhile this documentation provides comprehensive details on custom resources & schema references, we recommend working directly with our team during implementation. Schema references are powerful but complex, and our team can provide guidance, best practices, and hands-on support to ensure a smooth integration.Please contact Reach to schedule a collaborative session for implementing custom resources and schemas in your integration.

Overview

The platform supports creating schemas that reference other schemas, allowing you to define a type once and reuse it across multiple schemas. This is achieved using JSON Schema’s $ref keyword along with our custom URI scheme reach:schemas/{schemaIdOrName}.
All schemas in the platform are fully compliant with JSON Schema, an industry standard for defining the structure of JSON data. If you’re new to JSON Schema, we recommend reviewing the official documentation to understand the syntax and capabilities.

URL-Safe Schema Names

Schema names must be URL-safe as they can be used in API endpoint paths. Names must follow these rules:
  • Can contain only letters (a-z, A-Z), numbers (0-9), hyphens (-), and underscores (_)
  • Cannot contain spaces or special characters
  • Must be unique within a platform (case-insensitive)
Examples of valid schema names:
  • Customer
  • Order_Items
  • shipping-method
  • product_variant_2023
Examples of invalid schema names:
  • Customer Schema (contains space)
  • Order & Items (contains special character)
  • User's Profile (contains apostrophe)
These restrictions ensure that schema names can be used directly in URLs without requiring encoding.
Schema lookups are case-insensitive, but the original capitalization is preserved. This means you cannot have both “Order” and “ORDER” as separate schemas, as they are considered the same schema for lookup purposes.

Schema Categories

When creating a schema, you must specify its category. The category determines how the schema will be used in the platform. In order for Reach functionality to work properly, we need to know what certain schemas represent. The following categories are available:
  • user_schema: Used for storing user-related information and profiles.
  • custom_schema: Used for data models which do not have any info related to billing or users.
  • contacts_schema: Used for storing contact information.
  • transactions_schema: Used for transaction-related data.
  • combined_schema: A special category that can contain both user data and event data. This is useful when you need to track both user information and billable event information in a single schema.
When choosing a schema category, consider:
  • The primary purpose of the data you’re storing
  • Whether the data represents a billable transaction
  • If you need to track both user and order/transaction event data together
  • Whether the data represents custom fields specific to your business needs

Using Schema Names in References

To reference other schemas use $ref properties in your jsonSchema. In $ref properties: You can use either schema IDs or schema names in your $ref values.
"recent_order": { "$ref": "reach:schemas/Order" }
Using schema names makes your schema definitions more readable and maintainable, as you don’t need to remember or look up UUIDs.
When using schema names in references, the referenced schemas must exist and be defined for the same platform. References are case-insensitive, so "$ref": "reach:schemas/order" and "$ref": "reach:schemas/Order" both work.

Creating a Schema with References

When creating a schema that references another schema, you need to:
  1. List the referenced schema IDs or names in the reference_schemas array
  2. Use the $ref keyword in your schema properties to reference the other schema

Example: Creating Order and Customer Schemas

First, create an Order schema:
// POST /partner/schema-definitions
{
  "name": "Order",
  "plural_name": "Orders",
  "description": "Schema for order information",
  "category": "billable_event_schema",
  "external_id_field": "id",
  "schema": {
    "type": "object",
    "properties": {
      "id": { "type": "string" },
      "order_number": { "type": "string" },
      "amount": { "type": "number" },
      "status": { "type": "string", "enum": ["pending", "completed", "cancelled"] },
      "date": { "type": "string" },
      "items": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "product_id": { "type": "string" },
            "quantity": { "type": "integer" },
            "price": { "type": "number" }
          }
        }
      }
    },
    "required": ["id", "order_number", "amount", "status"]
  }
}
Then, create a Customer schema that references the Order schema by name:
// POST /partner/schema-definitions
{
  "name": "Customer",
  "plural_name": "Customers",
  "description": "Schema for customer data including order information",
  "category": "user_schema",
  "external_id_field": "id",
  "reference_schemas": ["Order"],
  "schema": {
    "type": "object",
    "properties": {
      "id": { "type": "string" },
      "name": { "type": "string" },
      "email": { "type": "string" },
      "phone": { "type": "string" },
      "recent_order": { "$ref": "reach:schemas/Order" },
      "order_history": {
        "type": "array",
        "items": { "$ref": "reach:schemas/Order" }
      }
    },
    "required": ["id", "name", "email"]
  }
}

Updating a Schema to Reference Other Schemas

You can update an existing schema to reference other schemas:
// POST /partner/schema-definitions/Customer
{
  "description": "Updated schema for customer data with payment information",
  "reference_schemas": ["Order", "PaymentMethod"],
  "schema": {
    "type": "object",
    "properties": {
      "id": { "type": "string" },
      "name": { "type": "string" },
      "email": { "type": "string"},
      "phone": { "type": "string" },
      "recent_order": { "$ref": "reach:schemas/Order" },
      "payment_method": { "$ref": "reach:schemas/PaymentMethod" }
    },
    "required": ["id", "name", "email"]
  }
}
Note how we’re using schema names in both the URL and the references.

Creating Resources with Schema References

Using the /api/resources/:schemaDefinitionNameOrId endpoint, you can create or update resources with schema references. This endpoint automatically validates the data against the schema definition including any references.
// POST /api/resources/Customer
{
  "resource_external_id": "customer-789",
  "data": {
    "id": "customer-789",
    "name": "Jane Smith",
    "email": "jane@example.com",
    "phone": "555-987-6543",
    "recent_order": {
      "id": "order-123",
      "order_number": "ORD-123",
      "amount": 149.99,
      "status": "pending",
      "date": "2023-07-20T15:45:00Z",
      "items": [
        {
          "product_id": "prod-002",
          "quantity": 1,
          "price": 149.99
        }
      ]
    }
  }
}
This will:
  1. Validate the data against the schema definition (including references)
  2. Create a new resource or update an existing one if the external ID already exists
  3. Return the created/updated resource with references resolved

Using External IDs for References

When referencing other resources in your data, you have two options:
  1. Embedded Objects (for validation only): You can include complete objects for validation.
  2. String References (for production use): For actual resource creation, you should use string references.
When creating resources that reference other resources, you should use the external ID of the referenced resource instead of embedding the complete object:
// POST /api/resources/Customer
{
  "resource_external_id": "customer-789",
  "data": {
    "id": "customer-789",
    "name": "Bob Johnson",
    "email": "bob@example.com",
    "phone": "555-111-2222",
    "recent_order": "order-456",  // Reference to an existing order by its external ID
    "order_history": ["order-456", "order-789", "order-123"]  // Array of order references
  }
}
This approach ensures that:
  1. Each resource is properly tracked and managed independently
  2. Updates to referenced resources are automatically reflected
  3. Data consistency is maintained across your system
The referenced resources (in this case, order-456, etc.) must already exist in the system. If they don’t, the validation will fail.

Response with Resolved References

When retrieving resources using the /api/resources/:schemaDefinitionNameOrId/:externalId endpoint with the include parameter, the system will automatically resolve references to their full objects:
// GET /api/resources/Customer/customer-789?include=recent_order,order_history
{
  "data": {
    "id": "resource-uuid",
    "business_id": "business-uuid",
    "platform_id": "platform-uuid",
    "schema_definition_id": "schema-uuid",
    "resource_external_id": "customer-789",
    "data": {
      "id": "customer-789",
      "name": "Bob Johnson",
      "email": "bob@example.com",
      "phone": "555-111-2222",
      "recent_order": "order-456", // Reference ID
      "order_history": ["order-456", "order-789", "order-123"] // Reference IDs
    },
    "created_at": "2023-07-01T12:00:00Z",
    "updated_at": "2023-07-01T12:00:00Z"
  },
  "include": {
    "recent_order": {  // Resolved from "order-456" to the full object
      "id": "order-456",
      "order_number": "ORD-456",
      "amount": 99.99,
      "status": "completed",
      "date": "2023-07-15T10:30:00Z",
      "items": [
        {
          "product_id": "prod-001",
          "quantity": 2,
          "price": 49.99
        }
      ]
    },
    "order_history": [
      {
        "id": "order-456",
        "order_number": "ORD-456",
        "amount": 99.99,
        "status": "completed",
        "date": "2023-07-15T10:30:00Z"
      },
      {
        "id": "order-789",
        "order_number": "ORD-789",
        "amount": 199.99,
        "status": "completed",
        "date": "2023-05-20T08:15:00Z"
      },
      {
        "id": "order-123",
        "order_number": "ORD-123",
        "amount": 149.99,
        "status": "pending",
        "date": "2023-07-20T15:45:00Z"
      }
    ]
  }
}

Getting All Resources for a Schema

To retrieve all resources for a particular schema, use the /api/resources/:schemaDefinitionNameOrId endpoint:
GET /api/resources/Customer
This will return an array of all resources for the Customer schema. You can use the include query parameter to automatically resolve references:
GET /api/resources/Customer?include=recent_order,order_history

Filtering Resources

You can filter resources based on their properties using the filters query parameter:
GET /api/resources/Customer?filters[email]=john@example.com
You can also filter on referenced fields:
GET /api/resources/Customer?filters[recent_order]=order-456

Filtering Examples

Here are examples of how to use filters to query your resources:

Basic Filtering

To find all customers with a specific email:
GET /api/resources/Customer?filters[email]=john@example.com
To find all orders with a specific status:
GET /api/resources/Order?filters[status]=completed

Multiple Filters

You can combine multiple filters to narrow down results:
GET /api/resources/Customer?filters[city]=New%20York&filters[state]=NY
This will return all customers from New York City in the state of NY. Note that values containing spaces or special characters need to be URL encoded (e.g., a space becomes %20).

Filtering with References

You can filter resources based on their relationships to other resources:
GET /api/resources/Order?filters[customer_id]=customer-123
This will return all orders for the customer with ID “customer-123”.

Combining Filters with Includes

You can filter resources and include referenced resources in the same request:
GET /api/resources/Order?filters[status]=completed&include=customer_id
This will return all completed orders and include the customer data for each order.

Using Arrays of Referenced Types

You can also define arrays of referenced types:
{
  "order_history": {
    "type": "array",
    "items": {
      "$ref": "reach:schemas/Order"
    }
  }
}
This allows you to have properties like:
// For validation:
{
  "id": "customer-789",
  "name": "Organization Name",
  "order_history": [
    {
      "id": "order-001",
      "order_number": "ORD-001",
      "amount": 99.99,
      "status": "completed",
      "date": "2023-06-15T14:30:00Z"
    },
    {
      "id": "order-002",
      "order_number": "ORD-002",
      "amount": 149.99,
      "status": "completed",
      "date": "2023-07-10T09:45:00Z"
    }
  ]
}

// For resource creation/updates:
{
  "id": "customer-789",
  "name": "Organization Name",
  "order_history": ["order-001", "order-002"]  // Reference by external IDs
}

Nested References

The system supports nested references, where a schema references another schema that in turn references a third schema. Just make sure to include all necessary schema IDs in the reference_schemas array. For example, a Company schema might reference both Customer and Order schemas:
{
  "name": "Company",
  "plural_name": "Companies",
  "description": "Schema for company data with primary customer and recent order",
  "category": "custom_schema",
  "external_id_field": "id",
  "reference_schemas": ["Customer", "Order"],
  "schema": {
    "type": "object",
    "properties": {
      "id": { "type": "string" },
      "name": { "type": "string" },
      "primary_customer": { "$ref": "reach:schemas/Customer" },
      "latest_order": { "$ref": "reach:schemas/Order" }
    },
    "required": ["id", "name", "primary_customer"]
  }
}

Schema Description Field

All schemas support a required description field that provides details about the schema’s purpose and usage. This makes it easier for developers to understand the schema’s intent and improves documentation. The description should:
  • Clearly explain the purpose of the schema
  • Include details about any special handling or business rules
  • Be concise but informative
This description is stored with the schema and can be retrieved along with other schema details.

Schema Mappings

For integrating your schemas with Reach functionality, you can define schema mappings that tell the system which fields in your schemas correspond to standard Reach concepts. Use the schema mapping endpoints to configure these mappings:
GET /partner/schema-mappings
To update your schema mappings:
// POST /partner/schema-mappings
{
  "contactsSchema": {
    "primarySource": {
      "type": "contacts_schema",
      "schemaId": "Customer",
      "email": "email",
      "phone": "phone",
      "userId": "id",
      "firstName": "first_name",
      "lastName": "last_name"
    }
  },
  "transactionsSchema": [
    {
      "type": "transactions_schema",
      "schemaId": "Order",
      "eventType": "order",
      "id_field": "id",
      "userId": "customer_id",
      "status": {
        "field": "status",
        "paidValue": "completed",
        "unpaidValue": "pending",
        "cancelledValue": "cancelled"
      },
      "amount": {
        "field": "amount",
        "currency": "USD"
      },
      "dates": {
        "createdDate": "created_at",
        "paidDate": "paid_at"
      }
    }
  ]
}

Implementation Details

Under the hood, the schema reference system works by:
  1. Storing referenced schema IDs in the reference_schemas field of the schema definition
  2. When validating data:
    • The system loads all referenced schemas
    • Registers them with AJV (the JSON Schema validator)
    • Uses a custom URI resolver to handle the reach:schemas/ prefix
    • Validates the data against both the main schema and any referenced schemas
  3. When retrieving resources, the system automatically resolves references by:
    • Identifying referenced schema properties in the data
    • Looking up the referenced resources by their external IDs
    • Replacing the reference IDs with the full resource data in the include object
This approach ensures consistent validation while keeping schemas modular and reusable.

Limitations

  1. Circular references are not currently supported
  2. All referenced schemas must exist in the system
  3. A schema cannot be deleted if it’s referenced by other schemas
  4. References are validated at runtime, so performance may be impacted with deeply nested references
  5. Referenced resources must exist before they can be referenced

Best Practices

  1. Create common types like Order, Product, etc. as separate schemas
  2. Use descriptive names for your schemas to make references clear
  3. Keep referenced schemas focused on a single concept
  4. Document your schema structure for other developers
  5. Include an ID field in all schemas to allow for easier resource management
  6. Minimize nesting depth to avoid performance issues
  7. Always create referenced resources before creating resources that reference them
  8. Use string IDs (not embedded objects) when creating resources with references
  9. Use schema names in URLs instead of IDs for better readability and maintainability
  10. Add clear descriptions to each schema to document its purpose and usage
  11. When retrieving resources with references, use the include parameter to specify which references should be resolved
  12. Use the filters parameter to efficiently find resources matching specific criteria