SchemaSmith Documentation

Custom Properties

Attach your own metadata to tables, columns, and indexes — survives re-extraction, drives behavior at deployment, and reaches every expression field across SQL Server, PostgreSQL, and MySQL.

By the SchemaSmith Team · Last reviewed

Custom Properties

Custom properties are how you attach your own metadata to the things in a schema package.

The Extensions Carrier

Every custom value lives inside a single property on its containing object called Extensions. It's an open JSON bag: objects, arrays, strings, numbers, booleans — anything the schema accepts. The core properties stay exactly where they've always been; your data rides in the Extensions object alongside them.

Table-level custom properties

{
  "Schema": "[dbo]",
  "Name": "[Orders]",
  "Columns": [],
  "Extensions": {
    "Environment": "Production",
    "DataClassification": "PII",
    "OwningTeam": "Identity"
  }
}

Column-level custom properties

{
  "Name": "[Amount]",
  "DataType": "DECIMAL(18,2)",
  "Nullable": false,
  "Extensions": {
    "DataClassification": "Financial",
    "MaskInNonProd": "true"
  }
}

Nested objects and arrays

Nested objects and arrays work naturally:

{
  "Name": "[Orders]",
  "Extensions": {
    "Retention": {
      "Policy": "7years",
      "Tier": "Hot"
    },
    "ResponsibleTeams": ["Billing", "Compliance"]
  }
}

That's all there is to storage. One property name, one JSON bag, anywhere you want to attach metadata.

Why a single carrier?

In earlier versions of SchemaSmith, custom properties were sprinkled directly into the object alongside the standard fields. The current design consolidates them into Extensions for two concrete reasons:

  1. Zero collision risk. You will never accidentally shadow a reserved property name. If a future SchemaSmith release adds a new top-level field called Environment, and you happened to be using Environment as a custom property, the flat form would silently overwrite your value. Under Extensions, your data is partitioned from the engine's data forever.
  2. Predictable schema generation. SchemaSmith generates the .json-schemas/*.schema validation files on the fly from the live C# type definitions. Every time those files are regenerated, they reflect exactly the current standard properties. Your Extensions content is never touched, never clobbered, and never silently validated against properties it doesn't belong to.

Supported Objects

Extensions is available on every table component type where it makes sense. The specific set varies by platform because the underlying schema elements vary.

All platforms

Object Lives in
Table Top-level table definition file
Column Columns array
Index Indexes array
ForeignKey ForeignKeys array
CheckConstraint CheckConstraints array

Platform-specific additions

Each platform contributes a small set of additional component types that also carry Extensions.

Object Lives in
XmlIndex XmlIndexes array
Statistic Statistics array
FullTextIndex FullTextIndex object
IndexedView Top-level indexed view definition file
IndexedView Index Indexes array inside an indexed view definition
Object Lives in
MaterializedView Top-level materialized view definition file
MaterializedView Index Indexes array inside a materialized view definition
ExcludeConstraint ExcludeConstraints array
Statistic Statistics array
Object Lives in
FullTextIndex FullTextIndexes array

Custom properties at any level are completely independent — a DataClassification on the table and a DataClassification on a column are two different values.

Note

Product.json and Template.json do not support Extensions. Custom properties belong to schema components, not to the product or template configuration.

Token Integration

Here's where custom properties stop being passive metadata and start driving behavior. When SchemaQuench processes a table, it walks Extensions on every component and produces {{TokenName}} substitutions you can reference in any expression field.

Scope rules

  • Table-level Extensions are available inside the table's own ShouldApplyExpression using bare names ({{Environment}}).
  • Table-level Extensions are also available in every child component's expression fields using the Table. prefix ({{Table.Environment}}).
  • Component-level Extensions are available in that component's own expression fields using bare names ({{MaskInNonProd}}).
  • Within a component's expression, both the component's own bare-name tokens and the parent table's Table.-prefixed tokens are merged. If a bare name collides with a table token, the component wins.

Flattening rules

  • Scalar values (string, number, bool) become direct token values.
  • Nested objects flatten with dot notation: Extensions.Retention.Policy becomes {{Retention.Policy}} at table scope or {{Table.Retention.Policy}} from child components.
  • Arrays become comma-joined strings: ["Billing", "Compliance"] becomes Billing,Compliance.
  • Token names are matched case-insensitively.

Where tokens are substituted

Custom property tokens resolve anywhere script tokens resolve — ShouldApplyExpression, Default, CheckExpression, script body text, and so on. See Script Tokens for the exhaustive list.

Example — environment-conditional index

{
  "Schema": "[dbo]",
  "Name": "[Orders]",
  "Extensions": {
    "Environment": "Production"
  },
  "Indexes": [
    {
      "Name": "[IX_Orders_CreatedDate]",
      "IndexColumns": "[CreatedDate]",
      "ShouldApplyExpression":
        "SELECT CASE WHEN '{{Table.Environment}}' = 'Production' THEN 1 ELSE 0 END"
    }
  ]
}

At quench time, the index applies only on the database whose deployment is flagged Production. No per-environment file copies. No branching. One declaration, one behavior, switched by a sidecar value your team controls.

Example — nested retention value driving a default

{
  "Schema": "[dbo]",
  "Name": "[Documents]",
  "Extensions": {
    "Retention": {
      "ArchiveDays": "90"
    }
  },
  "Columns": [
    {
      "Name": "[ArchiveAfterDays]",
      "DataType": "INT",
      "Nullable": false,
      "Default": "{{Table.Retention.ArchiveDays}}"
    }
  ]
}

Example — access from the component's own Extensions

{
  "Name": "[SSN]",
  "DataType": "VARCHAR(11)",
  "Nullable": true,
  "Extensions": {
    "PII": "true"
  },
  "ShouldApplyExpression":
    "SELECT CASE WHEN '{{PII}}' = 'true' AND '{{Table.Environment}}' = 'NonProd' THEN 0 ELSE 1 END"
}

Bare names pull from the column's own Extensions; Table.-prefixed names climb to the parent table.

Preservation During Re-extraction

SchemaTongs is a read-first tool. When it writes a table file back to a package that already contains a table file for the same table, it preserves whatever Extensions you had on the previous file. Your custom metadata survives the round-trip.

Matching is done by the component's Name property (with brackets/quotes stripped and case-insensitive comparison). Columns and the table itself also fall back to OldName, so renamed components keep their custom metadata as long as OldName is set correctly before the refresh.

Component Matched by Applies to
Table root object All platforms
Column Name, then OldName All platforms
Index Name All platforms
ForeignKey Name All platforms
CheckConstraint Name All platforms
XmlIndex Name SQL Server
FullTextIndex Name SQL Server, MySQL
ExcludeConstraint Name PostgreSQL
Statistic Name PostgreSQL
MaterializedView root object PostgreSQL
IndexedView root object SQL Server

If you drop a component from the database between extractions, its Extensions disappears with it — there's nothing to match against in the new file.

JSON Schema Validation

SchemaSmith generates validation schemas (.json-schemas/*.schema) for Product.json, Template.json, table JSON, materialized view JSON, and indexed view JSON on the fly from the current C# type definitions. Every time SchemaTongs extracts a package, those files are written fresh based on the current engine.

Extensions is an open JToken — the generated schema intentionally imposes no structure on it. That keeps the engine out of the business of validating your data.

If you want editor validation for your Extensions shape, you can hand-edit the relevant .schema file and add a JSON Schema fragment under the Extensions property. When SchemaTongs regenerates that schema file, it will preserve your custom Extensions definition and merge it back into the newly generated schema. Your validation rules outlive the regeneration cycle.

Example — require a DataClassification value

Tightening Extensions on tables.schema to require a DataClassification value:

{
  "properties": {
    "Extensions": {
      "type": "object",
      "required": ["DataClassification"],
      "properties": {
        "DataClassification": {
          "type": "string",
          "enum": ["Public", "Internal", "Confidential", "PII", "Financial"]
        },
        "OwningTeam": { "type": "string" }
      }
    }
  }
}

Drop that into .json-schemas/tables.schema under properties.Extensions (merging with whatever the schema already contains), and your editor enforces it for every table file in the package. SchemaTongs will carry your fragment forward on the next extraction; it will not re-validate the Extensions content itself.

No GUI property builder

SchemaSmith treats Extensions as a data-only feature — you edit the JSON directly, you hand-author the optional Extensions schema fragment, and you consume the values through script tokens. That's intentional: it keeps the tooling focused on what every team needs. Schema authorship, not form design.

Tool Interactions

SchemaTongs

Re-extraction preserves Extensions on every supported component as described above. Anything you add manually survives subsequent schema refreshes so long as the component name is unchanged (or OldName is set for renames).

SchemaQuench

Reads Extensions at quench time and resolves the contents as script tokens in expression fields. Extensions has no direct effect on DDL generation; its influence is entirely through the token substitution mechanism. The full table metadata — including Extensions — is available at runtime through the {{TableSchema}} automatic token if you need to emit it into a migration script or audit row.

DataTongs

Table files are round-tripped transparently; Extensions is neither inspected nor modified.

Naming Guidance

Because everything lives inside the Extensions bag, there are no reserved names to worry about — you cannot collide with a standard property. That said, keep your names stable and descriptive. The names become script tokens across your entire deployment, and renaming a property is a search-and-replace across every expression field that uses it.

A few practices that hold up well:

  • Pick one vocabulary per team and commit it to writing. DataClassification and dataClassification and data_classification will all work, but consistency keeps the tokens predictable.
  • Group related values under a nested object rather than flattening everything. Extensions.Retention.Policy reads better across a big schema than Extensions.RetentionPolicy, and it gives you a natural place to add related fields later.
  • Reserve namespace-style prefixes for shared metadata. If multiple products in the same repo have common metadata needs, a Company. or Compliance. prefix prevents accidental reuse across unrelated domains.