Schema CRUD Operations

Open inAnthropic

Prerequisites

What you'll achieve By the end of this guide, you will be able to create, inspect, modify, and delete schema classes in TerminusDB using the HTTP API, TypeScript client, or Python client.

Schema defines the structure of your documents — their types, properties, keys, and relationships. All schema operations use the Document API with graph_type=schema. This guide covers the full lifecycle: creating classes, inspecting existing schema, updating fields, and removing types.


Example database

Connect to TerminusDB first.

Create schema classes

Add new document types to your database. Each class is a JSON object with @type: "Class".

Example: TypeScript
const createSchema = async () => {
  const schema = [
    {
      "@type": "Class",
      "@id": "Country",
      "@key": { "@type": "Lexical", "@fields": ["name"] },
      "name": "xsd:string",
      "population": { "@type": "Optional", "@class": "xsd:integer" },
    },
    {
      "@type": "Class",
      "@id": "Person",
      "@key": { "@type": "Lexical", "@fields": ["name"] },
      "name": "xsd:string",
      "age": "xsd:integer",
      "nationality": "Country",
    },
  ]

  const result = await client.addDocument(schema, { graph_type: "schema" })
  console.log("Created:", result)
  // ["terminusdb:///schema#Country", "terminusdb:///schema#Person"]
}

Schema changes create a new commit. If instance documents already exist that violate the new schema, the operation will fail. Use schema migration to transform existing data during schema changes.


Read schema (inspect classes)

Retrieve your schema to inspect existing classes, their fields, and relationships. Use the same Document API with graph_type=schema for GET operations.

List all schema classes

Example: TypeScript
const listSchema = async () => {
  const classes = await client.getDocument({
    graph_type: "schema",
    as_list: true,
  })
  console.log("Schema classes:", classes)
}

Returns all schema objects including the @context:

Example: JSON
[
  {
    "@type": "@context",
    "@schema": "terminusdb:///schema#",
    "@base": "terminusdb:///data/"
  },
  {
    "@type": "Class",
    "@id": "Country",
    "@key": {"@type": "Lexical", "@fields": ["name"]},
    "name": "xsd:string",
    "population": {"@type": "Optional", "@class": "xsd:integer"}
  },
  {
    "@type": "Class",
    "@id": "Person",
    "@key": {"@type": "Lexical", "@fields": ["name"]},
    "name": "xsd:string",
    "age": "xsd:integer",
    "nationality": "Country"
  }
]

Get a single class definition

Example: TypeScript
const getClass = async () => {
  const personClass = await client.getDocument({
    graph_type: "schema",
    id: "Person",
  })
  console.log("Person class:", personClass)
}

Returns:

Example: JSON
{
  "@type": "Class",
  "@id": "Person",
  "@key": {"@type": "Lexical", "@fields": ["name"]},
  "name": "xsd:string",
  "age": "xsd:integer",
  "nationality": "Country"
}

Filter by type

Retrieve only classes (excluding the @context object):

Example: TypeScript
const getClassesOnly = async () => {
  const classes = await client.getDocument({
    graph_type: "schema",
    type: "Class",
    as_list: true,
  })
  console.log("Document classes:", classes)
}

Update schema (modify classes)

Update an existing class definition using PUT (full replacement of the class document). The class @id identifies which class to update.

Add a field to an existing class

To add an optional field, simply PUT the class with the new field included. Optional fields are backward-compatible — existing documents remain valid.

Example: TypeScript
const addOptionalField = async () => {
  const updatedPerson = {
    "@type": "Class",
    "@id": "Person",
    "@key": { "@type": "Lexical", "@fields": ["name"] },
    "name": "xsd:string",
    "age": "xsd:integer",
    "nationality": "Country",
    "email": { "@type": "Optional", "@class": "xsd:string" },
  }

  await client.updateDocument(updatedPerson, { graph_type: "schema" })
  console.log("Added email field to Person")
}

Adding a required field (not wrapped in Optional) is a strengthening operation. It will fail if documents of that type already exist — they would lack the new required field. Use schema migration with a CreateClassProperty operation and a default value instead.

Change a field type (schema weakening)

Widening a type (e.g., from a specific class to a more general one) is a weakening operation and succeeds directly:

Example: TypeScript
const widenField = async () => {
  // Change nationality from required "Country" to optional
  const updatedPerson = {
    "@type": "Class",
    "@id": "Person",
    "@key": { "@type": "Lexical", "@fields": ["name"] },
    "name": "xsd:string",
    "age": "xsd:integer",
    "nationality": { "@type": "Optional", "@class": "Country" },
    "email": { "@type": "Optional", "@class": "xsd:string" },
  }

  await client.updateDocument(updatedPerson, { graph_type: "schema" })
  console.log("Made nationality optional")
}

Schema weakening (making fields optional, adding optional fields, adding new classes) always succeeds because existing data still conforms. Schema strengthening (making fields required, narrowing types, removing fields) requires migration. See What is Schema Weakening? for details.


Delete schema classes

Remove a class from your schema. You must first remove all instance documents of that type and any properties in other classes that reference it.

Delete a class

Example: TypeScript
const deleteClass = async () => {
  await client.deleteDocument({
    graph_type: "schema",
    id: "Country",
  })
  console.log("Deleted Country class")
}

Deletion order matters. You cannot delete a class that is referenced by other classes. First remove or update properties that reference the class, then delete it. If instance documents of the class exist, delete those first.

Safe deletion sequence

When removing a class that is referenced elsewhere:

Example: TypeScript
const safeDeleteCountry = async () => {
  // 1. Remove field referencing Country from Person
  const updatedPerson = {
    "@type": "Class",
    "@id": "Person",
    "@key": { "@type": "Lexical", "@fields": ["name"] },
    "name": "xsd:string",
    "age": "xsd:integer",
    "email": { "@type": "Optional", "@class": "xsd:string" },
    // nationality removed
  }
  await client.updateDocument(updatedPerson, { graph_type: "schema" })

  // 2. Delete all Country instance documents
  const countries = await client.getDocument({ type: "Country", as_list: true })
  if (countries.length > 0) {
    const ids = countries.map((c: { "@id": string }) => c["@id"])
    await client.deleteDocument({ id: ids })
  }

  // 3. Now safe to delete the class
  await client.deleteDocument({ graph_type: "schema", id: "Country" })
  console.log("Country class safely deleted")
}

Full schema replacement

Replace the entire schema in one operation. This is useful for CI/CD pipelines or programmatic schema management where you maintain the schema as code.

Full replacement overwrites all schema documents. If your new schema is incompatible with existing instance data, the operation will fail. Use this for new databases or with schema migration for existing data.

Replace all schema classes

Example: TypeScript
const replaceFullSchema = async () => {
  const fullSchema = [
    {
      "@type": "@context",
      "@schema": "terminusdb:///schema#",
      "@base": "terminusdb:///data/",
    },
    {
      "@type": "Class",
      "@id": "Product",
      "@key": { "@type": "Lexical", "@fields": ["sku"] },
      "sku": "xsd:string",
      "name": "xsd:string",
      "price": "xsd:decimal",
      "category": { "@type": "Optional", "@class": "xsd:string" },
    },
    {
      "@type": "Class",
      "@id": "Order",
      "@key": { "@type": "Random" },
      "product": "Product",
      "quantity": "xsd:integer",
      "placed_at": "xsd:dateTime",
    },
  ]

  await client.updateDocument(fullSchema, {
    graph_type: "schema",
    create: true,
  })
  console.log("Full schema replaced")
}

The @context object

When replacing the full schema, always include the @context object first. It defines the URI prefixes for your schema and instance data:

Example: JSON
{
  "@type": "@context",
  "@schema": "terminusdb:///schema#",
  "@base": "terminusdb:///data/",
  "xsd": "http://www.w3.org/2001/XMLSchema#"
}
FieldPurposeDefault
@schemaURI prefix for class namesterminusdb:///schema#
@baseURI prefix for document IDsterminusdb:///data/
Custom prefixesAdditional URI namespaces(none)

If you omit @context from a full replacement, TerminusDB uses the defaults. Custom prefixes you previously defined will be lost.


Schema migration (breaking changes)

When schema changes conflict with existing instance data, use the Migration API to transform data automatically. Migrations handle operations that direct PUT cannot:

  • Adding required fields (with default values for existing documents)
  • Renaming properties (preserving data)
  • Casting field types (with type conversion)
  • Deleting classes (removing instance data)

Example: add a required field with a default

curl -u admin:root -X POST "http://localhost:6363/api/migration/admin/tdb-example-mydb" \
  -H "Content-Type: application/json" \
  -d '{
    "author": "admin",
    "message": "Add required sku field with default",
    "operations": [
      {
        "@type": "CreateClassProperty",
        "class": "Product",
        "property": "sku",
        "type": "xsd:string",
        "default": {"@type": "Default", "value": "UNKNOWN"}
      }
    ]
  }'

All existing Product documents receive "sku": "UNKNOWN" automatically.

Preview with dry-run

Test migrations without applying them:

curl -u admin:root -X POST \
  "http://localhost:6363/api/migration/admin/tdb-example-mydb?dry_run=true" \
  -H "Content-Type: application/json" \
  -d '{
    "author": "admin",
    "message": "Preview migration",
    "operations": [
      {"@type": "CreateClassProperty", "class": "Product", "property": "sku", "type": "xsd:string", "default": {"@type": "Default", "value": "UNKNOWN"}}
    ]
  }'

For the full migration operation reference, see Schema Migration Reference.


Common patterns

Schema as code (CI/CD)

Maintain your schema in version control and apply it on deployment:

Example: TypeScript
import { readFileSync } from "fs"

const deploySchema = async () => {
  const schema = JSON.parse(readFileSync("./schema.json", "utf-8"))
  await client.updateDocument(schema, {
    graph_type: "schema",
    create: true,
  })
  console.log("Schema deployed from schema.json")
}

Inspect schema changes (diff)

Compare schema between branches to review changes before merging:

curl -u admin:root \
  "http://localhost:6363/api/diff/admin/tdb-example-mydb/local/branch/main/local/branch/feature?document_id=Person&graph_type=schema"

Branch-based schema development

Develop schema changes on a branch, test them, then merge:

Example: TypeScript
const schemaOnBranch = async () => {
  // Create a feature branch
  await client.branch("schema-update")
  client.checkout("schema-update")

  // Make schema changes on the branch
  const newClass = {
    "@type": "Class",
    "@id": "Address",
    "@key": { "@type": "Random" },
    "street": "xsd:string",
    "city": "xsd:string",
    "postcode": "xsd:string",
  }
  await client.addDocument(newClass, { graph_type: "schema" })

  // Test, then merge back to main
  client.checkout("main")
  await client.rebase({ rebase_from: "admin/tdb-example-mydb/local/branch/schema-update" })
}

Next steps

Was this helpful?