SchemaView

The shape of data in iModels is expressed using ECSchemas. Sometimes, these schemas can grow quite large and complex. It is possible to have a hundred schemas, thousands of classes and hundreds of thousands of properties flat, which expands to millions of properties when you include inherited properties. When that happens, performance and memory consumption suffer.

SchemaView is the first library primarily aimed at performance and memory consumption. It uses a binary blob to fetch exactly the data it needs from the iModel in as few (async) calls as possible, including string and property deduplication. It is read-only and designed for synchronous access to schema metadata that is held in memory for the lifetime of a connection.

It lives in @itwin/ecschema-metadata and should be the first choice for accessing schema metadata at runtime - for example in presentation layers, property grids, or data-driven UI.

For the binary transport format specification, see SchemaViewBinaryFormat.md.

SchemaView excludes some information like units, formats, and custom attribute instances. For the full, authoritative EC schema model and its interchange formats, see ECSchema XML and ECSchema JSON. For formats and units, a separate dedicated API is planned for the near future. For custom attributes, a sidecar will be provided alongside SchemaView which allows getting them on demand directly from the iModel.

When to use SchemaView

Use SchemaView when you need fast, synchronous, repeated lookups at runtime without chatty calls into the iModel.

Reach for the full-fidelity SchemaContext instead when you are: authoring, validating, serializing to XML/JSON, or accessing data that SchemaView deliberately omits (see What is included). SchemaContext is the more expensive option - a full object graph with cross-references - use it when its completeness is what you actually need.

What is included

SchemaView covers what runtime consumers ask for most: schemas, classes (entity, struct, mixin, relationship, custom attribute, view), properties (including inherited, in declaration order), enumerations, kinds of quantity, property categories, and relationship constraints. Class-type checks and downward navigation via derivedClasses are also exposed.

A small number of widely-used custom attributes are promoted to first-class concepts on the view objects:

  • Views - ECViews are surfaced as their own ClassType.View (see Views).
  • Mixin - the mixin custom attribute is reflected in classType and is included in applies-to/is-a walks.
  • Hidden - hidden flags on schemas, classes, and properties are exposed directly (e.g. schema.isHidden).

What is excluded

The exclusion list is a curated set of things we judged to be outside the sweet spot of what runtime consumers usually need. It is not primarily a size optimization - on a typical iModel the excluded data is a small fraction of the schema - it is about keeping the view focused, and keeping out data that has historically caused trouble. Custom attribute instances in particular have been a recurring source of performance problems in the heavier metadata layers: some iModels carry exceptionally large or numerous custom attributes that most apps never look at. Anything omitted is still reachable on demand via SchemaContext or ECDbMeta ECSQL queries. At a high level, SchemaView omits:

  • Custom attribute instances - the values, not the classes.
  • "Standard" schemas - units, formats, core custom attributes, and the EC2-era legacy schemas (as defined by ECObjects' ECSchema::IsStandardSchema). KindOfQuantity still carries its persistence-unit and presentation-format strings; you resolve those names yourself (see Resolving format and unit names).
  • ECDb-internal and pure custom-attribute schemas - storage-mapping schemas and decoration-only schemas whose only value is the CA instances that aren't transported anyway. ECDbMeta is not excluded - it stays queryable via ECSQL.

For the exact schema names in each category, see Full list of excluded schemas. The authoritative logic lives in IsExcludedSchema() in SchemaViewWriter, which delegates the standard-schema check to ECSchema::IsStandardSchema.

The list is subject to debate - if a compelling use case emerges for any excluded schema, we will remove it. When you do need data from an excluded schema, SchemaContext and ECDbMeta queries remain available.

Obtaining the schema view

The schema view is obtained from IModelDb (backend) or IModelConnection (frontend). The first call builds the cache; subsequent calls return it instantly.

// Obtain the schema view (async, cached after first call) const view = await iModel.getSchemaView(); // Subsequent calls return the cached view instantly const same = await iModel.getSchemaView(); assert.strictEqual(view, same);

The schema view is cached for the lifetime of the connection. Schema changes (via importSchemas or pulling changesets with schema changes) automatically invalidate the cache.

Loading only a subset of schemas

By default getSchemaView() loads every (non-excluded) schema in the iModel. On a large iModel with many schemas, a consumer that only needs a few - for example a Models tree that starts from BisCore - can ask for just those schemas plus their references:

// Load only a subset of schemas (plus their reference closure) instead of every schema. // This is an accumulating view instance that initially contains only the schemas you ask for. const bisOnly = await iModel.getSchemaView({ schemas: ["BisCore"] }); assert.isDefined(bisOnly.getSchema("BisCore")); assert.isDefined(bisOnly.findClass("BisCore:Element")); // A later request for more schemas merges them into the SAME accumulating instance - schemas // requested earlier remain available. Schemas that were never requested are simply absent. const withGeneric = await iModel.getSchemaView({ schemas: ["Generic"] }); assert.strictEqual(withGeneric, bisOnly); assert.isDefined(withGeneric.getSchema("BisCore")); // still here

We use a single accumulating SchemaView, so if multiple callers request different subsets, the union of all requested schemas is loaded. If one party loads the whole set, it will be cached for everybody despite what filters they requested. View the option like a "I care about these schemas" rather than "only return these".

The trade-off is: you pay only for the schemas you load, but downward schema navigation (derivedClasses) will only reflect what is loaded. Reach for the full view when you need a complete picture, but try to limit the scope when possible.

Additional instructions for backend/frontend developers: We strongly advise against loading the "full" SchemaView automatically on every connection. Try and keep this "on-demand" with limited scope, or else every consumer always pays the cost. That said, on backend, even the worst case scenarios should load in less than a second, asynchronously.

Navigating schemas and classes

All lookups are synchronous and case-insensitive.

// Look up a schema by name (case-insensitive) const bis = view.getSchema("BisCore"); assert.isDefined(bis); // Iterate all schemas in the context for (const schema of view.getSchemas()) { // Every schema has a name, alias, and version assert.isNotEmpty(schema.name); assert.isNotEmpty(schema.alias); assert.isAtLeast(schema.readVersion, 1); } // Look up a class by qualified name - both ":" and "." separators work const element = view.findClass("BisCore:Element"); const alsoElement = view.findClass("BisCore.Element"); assert.isDefined(element); assert.isDefined(alsoElement); assert.strictEqual(element!.fullName, alsoElement!.fullName); // Look up a class within a schema const model = bis!.getClass("Model"); assert.isDefined(model); assert.strictEqual(model!.schema.name, "BisCore");

Class types and IS-A checks

Classes expose their type (entity, relationship, struct, mixin, custom attribute, view) and support is() for inheritance checks. The is() method walks base classes and mixins transitively - the result is cached after the first call. Use isRelationship() to narrow to SchemaView.RelationshipClass for type-safe access to strength, direction, source, and target constraint fields.

const element = view.findClass("BisCore:Element")!; const geom3d = view.findClass("BisCore:GeometricElement3d")!; const modelContains = view.findClass("BisCore:ModelContainsElements")!; // Type guards assert.isTrue(element.isEntity()); assert.isTrue(modelContains.isRelationship()); // IS-A check - walks base classes and mixins transitively, result is cached assert.isTrue(geom3d.is(element)); // GeometricElement3d derives from Element assert.isTrue(geom3d.is("BisCore:Element")); // Also works with qualified name strings assert.isFalse(element.is(geom3d)); // Element does not derive from GeometricElement3d // Class hierarchy assert.isDefined(geom3d.baseClass); assert.strictEqual(geom3d.baseClass!.name, "GeometricElement");

Working with properties

Properties include inherited properties from base classes and mixins, in base-first declaration order. Each property exposes its kind (primitive, struct, array, navigation) and type-specific attributes.

const element = view.findClass("BisCore:Element")!; // Get all properties including inherited (base-first order) const allProps = element.getProperties(); assert.isAbove(allProps.length, 0); // Look up a specific property by name (case-insensitive) const codeValue = element.getProperty("CodeValue"); assert.isDefined(codeValue); assert.isTrue(codeValue!.isPrimitive()); assert.strictEqual(codeValue!.kind, PropertyKind.Primitive); // Property type information for (const prop of allProps) { if (prop.isPrimitive()) { // Primitives have a type and optionally an extended type name assert.notStrictEqual(prop.primitiveType, 0); } else if (prop.isNavigation()) { // Navigation properties have a direction and a relationship class assert.isDefined(prop.relationshipClass); } else if (prop.isStruct()) { // Struct properties reference a struct class assert.isDefined(prop.structClass); } }

Relationship constraints

Relationship classes expose source and target constraints, each with an abstract constraint class. Use assertRelationship() or isRelationship() to narrow to SchemaView.RelationshipClass before accessing these fields.

const modelContains = view.findClass("BisCore:ModelContainsElements")!; assert.isTrue(modelContains.isRelationship()); // Narrow to SchemaView.RelationshipClass for access to constraints if (modelContains.isRelationship()) { // Relationship classes have source and target constraints const source = modelContains.source; const target = modelContains.target; assert.isDefined(source); assert.isDefined(target); // Constraints have multiplicity, polymorphism, and constraint classes assert.isAbove(source!.constraintClasses.length, 0); assert.isAbove(target!.constraintClasses.length, 0); // Abstract constraint class is optional - not all relationships define one // When present, it restricts which classes can participate in the relationship if (target!.abstractConstraint !== undefined) assert.isNotEmpty(target!.abstractConstraint.name); }

Enumerations

Schemas contain enumerations with typed enumerators.

// Look up an enumeration within a schema const bis = view.getSchema("BisCore")!; for (const enumeration of bis.getEnumerations()) { // Enumerations have a name, primitiveType, and enumerators assert.isNotEmpty(enumeration.name); for (const enumerator of enumeration.getEnumerators()) { assert.isNotEmpty(enumerator.name); } }

Kind of quantity and property categories

Properties can reference a kind of quantity (KoQ) or a property category. KoQs carry presentation format information - presentationFormats returns parsed SchemaView.PresentationFormat objects with the format name, optional precision override, and optional unit/label overrides. All names are alias-qualified (e.g. "f:DefaultRealU", "u:M").

// Some properties reference a KindOfQuantity const bis = view.getSchema("BisCore")!; for (const koq of bis.getKindOfQuantities()) { assert.isNotEmpty(koq.name); assert.isNotEmpty(koq.fullName); // "SchemaName:KoqName" // Parsed presentation formats - each has the format name, optional precision, // and optional unit overrides (all alias-qualified, e.g. "f:DefaultRealU", "u:M"). for (const fmt of koq.presentationFormats) { assert.isNotEmpty(fmt.name); // fmt.precision - number or undefined // fmt.unitAndLabels - array of [unitName, labelOverride] tuples or undefined } // The raw JSON string is available for custom parsing // koq.presentationFormatsRaw - e.g. '["f:DefaultRealU(2)[u:M]","f:DefaultRealU(2)[u:FT]"]' } // Properties can reference a category const element = view.findClass("BisCore:Element")!; for (const prop of element.getProperties()) { const koq = prop.isPrimitive() ? prop.kindOfQuantity : undefined; if (koq !== undefined) { assert.isNotEmpty(koq.fullName); } }

Resolving format and unit names

Presentation format names use schema aliases (e.g. "f" for Formats, "u" for Units). The Units and Formats schemas themselves are excluded from SchemaView (see What is excluded), so you resolve these names yourself.

Pre-EC3.2 iModels: on the rare iModel still on ECDb profile 4.0.0.1 (predates the 2018 EC3.2 Units/Formats migration), KindOfQuantity.persistenceUnit and presentationFormats are returned in legacy FUS format and will not parse with the alias-qualified resolution patterns below. The fix is to upgrade the iModel's ECDb profile. See SchemaViewBinaryFormat - ECDb Profile Compatibility.

If you need the actual format definitions or unit details today, you can resolve the alias-qualified names via ecschema-metadata or ECSQL:

Split the alias-qualified name at : - the left part is the schema alias (e.g. "f" -> "Formats", "u" -> "Units"), the right part is the item name. Look up the schema item via IModelDb.schemaContext:

const fmt = koq.presentationFormats[0]; // { name: "f:DefaultRealU", precision: 2, unitAndLabels: [["u:M", undefined]] } // Resolve format: "f:DefaultRealU" -> "Formats.DefaultRealU" const format = iModel.schemaContext.getSchemaItemSync("Formats.DefaultRealU", Format); // format.precision, format.type, format.formatTraits, etc. // Resolve unit: "u:M" -> "Units.M" const unit = iModel.schemaContext.getSchemaItemSync("Units.M", Unit); // unit.fullName, unit.label, await unit.unitSystem, etc. // To build a FormatterSpec for quantity formatting, use SchemaUnitProvider: const unitsProvider = new SchemaUnitProvider(iModel.schemaContext); const persistenceUnit = await unitsProvider.findUnitByName("Units.M");

Via ECSQL (ECDbMeta)

Query meta.FormatDef for the format's NumericSpec (a JSON object with type, precision, traits, etc.) and meta.UnitDef for unit details:

// Look up the base format definition for await (const row of iModel.createQueryReader( `SELECT f.NumericSpec, f.CompositeSpec FROM meta.FormatDef f JOIN meta.ECSchemaDef s USING meta.SchemaOwnsFormats WHERE f.Name = 'DefaultRealU' AND s.Name = 'Formats'`, )) { /* row.numericSpec is a JSON string with FormatProps */ } // Look up a unit and its unit system for await (const row of iModel.createQueryReader( `SELECT u.Name, u.DisplayLabel, us.Name AS unitSystemName FROM meta.UnitDef u JOIN meta.ECSchemaDef s USING meta.SchemaOwnsUnits JOIN meta.UnitSystemDef us USING meta.UnitSystemHasUnits WHERE u.Name = 'M' AND s.Name = 'Units'`, )) { /* row.name, row.displayLabel, row.unitSystemName */ } // For composite formats (like AngleDMS with degrees/minutes/seconds), query the composite units: for await (const row of iModel.createQueryReader( `SELECT cu.Ordinal, u.Name AS unitName, cu.Label FROM meta.FormatCompositeUnitDef cu JOIN meta.FormatDef f USING meta.FormatOwnsCompositeUnits JOIN meta.ECSchemaDef s USING meta.SchemaOwnsFormats JOIN meta.UnitDef u USING meta.CompositeUnitRefersToUnit WHERE f.Name = 'AngleDMS' AND s.Name = 'Formats' ORDER BY cu.Ordinal`, )) { /* row.ordinal, row.unitName, row.label */ }

Views

ECViews (entity classes with a QueryView custom attribute) are included in the SchemaView as their own type distinct from entity classes. They show up as classes with ClassType.View - use schema.getClasses(ClassType.View) to iterate just views, or findClass(...) + isView() to look one up by qualified name. Views expose their own properties but do not participate in class inheritance.

// Iterate views within a schema - pass ClassType.View to filter to views only. for (const schema of schemaView.getSchemas()) { for (const view of schema.getClasses(ClassType.View)) { assert.isNotEmpty(view.name); assert.isNotEmpty(view.fullName); // "SchemaName:ViewName" // Views have their own properties (no inheritance) for (const prop of view.getProperties()) { assert.isNotEmpty(prop.name); } } } // Look up a view by qualified name - use findClass + isView() since views are // just classes with ClassType.View. // const cls = schemaView.findClass("SomeSchema:SomeView"); // const view = cls?.isView() ? cls : undefined;

Derived classes

You can walk the class hierarchy downward via derivedClasses. The reverse map is built lazily on first access.

const element = view.findClass("BisCore:Element")!; // Walk direct derived classes (reverse map built lazily on first call) const directDerived = element.derivedClasses; assert.isAbove(directDerived.length, 0); // Each derived class has Element as its base for (const derived of directDerived) { assert.isTrue(derived.is(element)); }

Exhaustive walk

You can iterate every schema, class, and property in the schema view efficiently. This is a common pattern for building indexes or validating metadata.

// Walk every class and every property in the entire context let totalClasses = 0; let totalProperties = 0; for (const schema of view.getSchemas()) { for (const cls of schema.getClasses()) { totalClasses++; const props = cls.getProperties(); // includes inherited totalProperties += props.length; // Every class must be one of these types. Pass a ClassType to // schema.getClasses(...) if you want to filter to one kind. assert.isTrue( cls.type === ClassType.Entity || cls.type === ClassType.Relationship || cls.type === ClassType.Struct || cls.type === ClassType.CustomAttribute || cls.type === ClassType.Mixin || cls.type === ClassType.View, ); } } // Even a minimal test iModel has BisCore schemas with hundreds of classes assert.isAbove(totalClasses, 0); assert.isAbove(totalProperties, 0);

Presentation-style adapter pattern

SchemaView is designed to replace ecschema-metadata in presentation and UI code. Here is a typical adapter pattern:

// Adapt SchemaView for use with presentation-style consumers // that need schema/class/property lookup. function classDerivesFrom(view: SchemaView, classFullName: string, baseFullName: string): boolean { const cls = view.findClass(classFullName); if (cls === undefined) return false; return cls.is(baseFullName); } // Check whether a class derives from BisCore:GeometricElement const isGeometric = classDerivesFrom(schemaView, "BisCore:GeometricElement3d", "BisCore:GeometricElement"); assert.isTrue(isGeometric); // Get the properties of a class for display const element = schemaView.findClass("BisCore:Element")!; const displayProps = element.getProperties() .filter((p) => !p.isHidden) .map((p) => ({ name: p.name, label: p.label, kind: p.kind })); assert.isAbove(displayProps.length, 0);

Sync/async contract

All schema, class, and property access is synchronous. getSchemaView() is the only asynchronous/IO step - it loads the binary blob once - and every read after that is synchronous. This is a key difference from ecschema-metadata, where loading schemas and resolving cross-references requires async calls and results in unpredictable loading behavior.

View objects and allocation

SchemaView.Schema, SchemaView.Class, SchemaView.Property, and the other view types are lightweight wrappers holding only a SchemaView reference and an index. They do not cache data and are not identity-stable - calling element.schema twice returns two distinct objects that expose the same data. This means === comparison will fail; use name or fullName for equality checks.

Calling getProperties() allocates a new SchemaView.Property wrapper for each property on every call. For hot loops, consider caching the result in a local variable. The underlying data is shared - only the thin wrapper objects are allocated.

Dangling references

Because some schemas are excluded wholesale (see What is excluded), cross-references pointing into them become unresolvable. The loader handles this as follows:

  • Struct and navigation properties whose type can't be resolved are dropped - they won't appear in the property list at all. This means structClass and relationshipClass are always valid (non-nullable) on any property you can see.
  • Base classes and mixins that can't be resolved are silently skipped - baseClass returns undefined, missing mixins are omitted from the mixin list.
  • Enumerations, categories, and kinds of quantity that can't be resolved result in undefined from the corresponding getter.

A diagnostic warning is logged listing all unresolved references. In practice, the current exclusion list produces very few dangling references because domain schemas rarely have structural dependencies on the excluded infrastructure schemas.

How does SchemaView differ from ECDbMeta ECSQL queries?

The ECDbMeta schema (meta.ECClassDef, meta.ECPropertyDef, etc.) exposes the same underlying ec_ tables via ECSQL. You can query individual classes or properties with SQL filters, joins, and projections. This is powerful for targeted lookups - for example, "find all navigation properties pointing at BisCore:Element."

SchemaView reads the same ec_ tables, but caches the curated subset in one shot into an in-memory structure optimized for traversal.

If you need "give me all classes where property X has extended type Y" - use ECSQL. If you need "walk the property list of this class including inherited properties and check each one" - use SchemaView.

At the time of writing, some concepts are not exposed through ECDbMeta, and some iModels may not have updated to its latest version which added CustomAttributes. Walking all flattened properties of a class is currently not something that ECDbMeta supports.

Full list of excluded schemas

Exact schema names excluded from SchemaView
  • Custom attribute instances (all).
  • All "standard" schemas as defined by ECObjects' ECSchema::IsStandardSchema:
    • EC3 standards: CoreCustomAttributes, Units, Formats, ECDbMap, SchemaLocalizationCustomAttributes, EditorCustomAttributes.
    • Legacy EC2-era schemas: Bentley_Standard_CustomAttributes, Bentley_Standard_Classes, Bentley_ECSchemaMap, Bentley_Common_Classes, Dimension_Schema, iip_mdb_customAttributes, KindOfQuantity_Schema, rdl_customAttributes, SIUnitSystemDefaults, Unit_Attributes, Units_Schema, USCustomaryUnitSystemDefaults. These predate EC3.2 and are not referenced structurally by modern domain schemas.
  • ECDb-internal schemas beyond the standard list - ECDbSystem, ECDbFileInfo, ECDbSchemaPolicies. These describe storage-layer mapping and are not relevant to runtime consumers.
  • Pure custom-attribute schemas beyond the standard list - BisCustomAttributes, ECv3ConversionAttributes, SchemaUpgradeCustomAttributes. These contain only CustomAttribute and Struct definitions used for decoration; since CA instances are not transported, the definitions add little value.

Schema Localization

To use schema localization for SchemaView items, see SchemaLocalization.md.

Last Updated: 28 July, 2026