Generated models and rich text

September 15, 2026 · View on GitHub

Authoring variants with generated content-type records instead of element records.

This guide is about CLR records that mirror your content types. Changing the content types themselves — the CMS schema — is content model.

Generate Management models

The model generator emits records from your existing content types; run it with -m / --management:

KontentModelGenerator --management --environmentId "<environmentId>" --apiKey "<management-api-key>" --outputdir "./Models"

The emitted records require Kontent.Ai.Management 9.0 or newer. Switches, output layout and the full emitted shape are the generator's own documentation — see Management Models.

Extend generated records in separate partial record files so your additions survive regeneration.

Examples below assume a configured IManagementClient client (see configuration) and an Article record generated from an article content type with these elements: title (text), body (rich text), post_date (date & time), slug (URL slug) and rating (custom). The inline-component example additionally assumes a callout type whose multiple-choice type element allows several selections and offers a warning option — that is why the generated Callout.Type is a collection.

Read and write typed variants

Pass a record straight to the upsert:

var identifier = LanguageVariantIdentifier.ByCodenames("on_roasts", "en-US");

var article = new Article
{
    Title = "On Roasts",
    PostDate = new DateTimeOffset(2018, 7, 4, 0, 0, 0, TimeSpan.Zero)
};

var result = await client.UpsertLanguageVariantAsync(identifier, article);

The generic get and upsert return LanguageVariantModel<T> — the typed Elements plus the same variant metadata the untyped model carries:

LanguageVariantModel<Article> variant =
    (await client.GetLanguageVariantAsync<Article>(identifier)).EnsureSuccess();

Article elements = variant.Elements;     // typed element values
Reference item = variant.Item;           // everything else is metadata
DateTime lastModified = variant.LastModified;

Listings whose variants all share one content type take the same type parameter — ListLanguageVariantsByItemAsync<T>, ListLanguageVariantsByTypeAsync<T>, ListLanguageVariantsOfContentTypeWithComponentsAsync<T>, and the …PageAsync<T> overloads of the last two:

var articles =
    (await client.ListLanguageVariantsByTypeAsync<Article>(Reference.ByCodename("article"))).EnsureSuccess();

foreach (var variant in articles)
{
    Console.WriteLine($"{variant.Language.Id}: {variant.Elements.Title}");
}

A listing by collection or space mixes content types, so it stays untyped. Once you know a variant's type, project it with the same conversion the typed calls use:

LanguageVariantModel<Article> article = client.ToTyped<Article>(variant);

Environment binding and partial updates

Important

A typed read matches elements by id, so a record works only against the environment it was generated from. A response in which no element matches throws InvalidOperationException — that is a mismatch between your model and the environment, not an outcome of the call. Elements the record does not know are skipped, so a record generated before a type gained an element keeps working.

Writes key off codenames and are portable across environments.

An upsert sends every element whose property value is not null, and omits the rest. Generated Management records have nullable properties and no initializers, so a fresh record sends only the elements you give non-null values:

var article = new Article { Title = "On Roasts" };     // sends title only
await client.UpsertLanguageVariantAsync(identifier, article);

The SDK checks current values; it does not track assignment. Omitting an element leaves it unchanged. That is not the same as clearing it.

Element records and model values

Two families, one per authoring path. You do not mix them.

Element recordsModel values
Used withLanguageVariantUpsertModel.Elements[]a generated record's properties
Carries its own Element referenceyes — it lives in an untyped arrayno — the property identifies the element
ExampleCustomElementCustomValue
Generator needednoyes

Most element kinds map to a bare value on a generated record:

Element kindElement recordGenerated property
TextTextElementstring?
NumberNumberElementdecimal?
Linked items, taxonomy, subpagesLinkedItemsElement, TaxonomyElement, SubpagesElementIEnumerable<Reference>?
AssetAssetElementIEnumerable<AssetReference>?
Multiple choiceMultipleChoiceElementgenerated enum — TEnum? for single-select, IEnumerable<TEnum>? for multi
Date & timeDateTimeElementDateTimeValue? — instant plus DisplayTimeZone
URL slugUrlSlugElementUrlSlugValue? — slug plus Mode
CustomCustomElementCustomValue? — value plus SearchableValue
Rich textRichTextElementRichTextValue? — HTML plus Components

The last four carry a companion field beside the value:

using Kontent.Ai.Management.Models.Content;   // UrlSlugMode

var article = new Article
{
    PostDate = new DateTimeValue
    {
        Value = new DateTimeOffset(2018, 7, 4, 0, 0, 0, TimeSpan.Zero),
        DisplayTimeZone = "Europe/Prague"
    },
    Slug = new UrlSlugValue { Value = "on-roasts", Mode = UrlSlugMode.Custom },
    Rating = new CustomValue { Value = "{\"stars\":5}", SearchableValue = "5 stars" }
};

Each has an implicit conversion for the common case, so PostDate = new DateTimeOffset(…), Slug = "on-roasts" (custom mode) and Rating = "{\"stars\":5}" all work when the companion field is not needed.

Important

A date & time is stored as a UTC instant; DisplayTimeZone only hints how an editor renders it and never changes the instant. The element takes a DateTimeOffset rather than a DateTime so the moment is unambiguous — a bare DateTime would be resolved against the machine's local zone. Whatever offset you supply is normalized to UTC on the wire.

For an element kind the SDK does not model, or to replay a variant fetched as raw JSON, DynamicElement writes its Value to the wire as-is:

new DynamicElement { Element = Reference.ByCodename("widget"), Value = "<opaque payload>" }

Rich text and inline components

Rich text is authored as an HTML string. RichTextBuilder handles the error-prone part: keeping each inline <object data-id="…"> placeholder in sync with the matching entry in the components array.

Interpolate helper calls into the HTML. The builder mints the shared GUID, emits the placeholder and records the component, so you never handle the GUID:

using Kontent.Ai.Management.Models.Content;

var rt = new RichTextBuilder();

var content = rt.Build($"""
    <h1>On Roasts</h1>
    <p>See {rt.ItemLink(Reference.ByCodename("intro"), "the introduction")} first.</p>
    {rt.Component(new Callout { Type = [CalloutType.Warning] })}
    {rt.Asset(new AssetReference { Codename = "roasting_chart" })}
    """);

var article = new Article { Title = "On Roasts", Body = content };
await client.UpsertLanguageVariantAsync(identifier, article);

Build returns a RichTextValue — verbatim HTML plus the recorded components — ready to assign to a rich-text property. Callout here is another generated record, embedded as an inline component.

HelperEmitsUse for
Component(IElementsModel item)<object data-type="component" data-id="…"> and records the componentEmbedding a generated record as an inline component
LinkedItem(Reference)<object data-type="item" data-…="…">Referencing an existing item inline
ItemLink(Reference, linkText)<a data-item-…="…">…</a>A hyperlink to an item
Asset(AssetReference)<figure data-asset-…="…">Embedding an asset

Component accepts generated content-type records. Interpolation evaluates left to right, so call order is record order. Build snapshots and resets the builder, so one instance can produce several elements in turn; a builder nested inside a component's own rich-text body is independent of the outer one.

Note

The HTML passes through verbatim — the builder does not sanitize or validate markup, and is intended for trusted, code-authored content such as migration scripts. Helper attribute values and link text are HTML-encoded.

Rendering rich text for a website is the Delivery SDK's job, not this one.