Add custom data types
Xperience registers a set of data types by default, covering common field content such as text, numbers, and dates. When none of the default types can represent the data you need to store, extend the set with a custom data type. For example, a custom data type can store a composite object, such as a serialized JSON structure, in a single database column.
Custom data types and headless channels
The GraphQL API for headless channels doesn’t support custom data types. Content types available through the API can only use the built-in scalar types. For more information, see Retrieve headless content.
Custom data type requirements
Registering the type is only the first step. On its own, a registered type has no name in the field editor, generates as object in code files, and has no component to edit its values. A complete custom data type consists of the following parts:
- A
DataType<T>registration that carries the conversion functions between the C# type and its database representation. See Register the data type. - Configuration that matches the storage format of the type,
SqlValueFormatandDbTypein particular. See Data type configuration. - A localization resource key with the name displayed by the field editor. See Set the display name.
- A code generator that supplies the C# type of generated properties. See Register a code generator.
- A default value editor, unless the registration sets the
HasConfigurableDefaultValueproperty tofalse. See Assign a default value editor. - A form component that edits values of the data type.
Register the data type
To register a custom data type, call the RegisterDataTypes(params DataType[]) method of the DataTypeManager class. Call the method during application startup, in the OnPreInit method of a module class.
The DataType<T> class provides multiple constructors. Which constructor to use depends on the complexity of the registered type.
For primitive types and simple objects that the system can directly serialize to the database, use:
DataType<T>(sqlType: string sqlType,
fieldType: string fieldType,
schemaType: string schemaType,
conversionFunc: Func<object, T, CultureInfo, T> conversionFunc)
where:
T– the generic type parameter. Substitute with the C# type that holds values of the data type.sqlType– the SQL type of the database column that stores the values.fieldType– the string identifier of the data type in the system. Must be lowercase.schemaType– the XSD data type (xs:string,xs:integer, etc.) used for XML serialization intoIDataContainer.conversionFunc– the function that converts the stored database value to the data type. Must be static.
For composite types, use:
DataType<T>(sqlType: string sqlType,
fieldType: string fieldType,
schemaType: string schemaType,
conversionFunc: Func<object, T, CultureInfo, T> conversionFunc,
dbConversionFunc: Func<T, object, CultureInfo, object> dbConversionFunc,
textSerializer: IDataTypeTextSerializer textSerializer)
In addition to the parameters shared with the basic constructor, provide:
dbConversionFunc– the function that converts the data type to its string representation for storage in the database, for example a JSON string. Must be static.textSerializer– anIDataTypeTextSerializerimplementation that serializes values of the data type to and from text. The system uses the serializer when storing values in field definitions, for example the property values of the form component assigned to a field. UseDefaultDataTypeTextSerializerunless the type requires custom handling, and instantiate the serializer with the samefieldTypeidentifier as the data type.
For composite types, schemaType is commonly xs:string, as the values are often stored as serialized objects, for example JSON strings.
Both constructors also have an overload with an additional final parameter:
validationFunction– aFunc<object, CultureInfo, bool>that determines whether an arbitrary object holds a value of the data type. If not provided, the system considers all values to be of the type.
For example, the composite type MyCompositeType:
/// <summary>
/// Example composite type stored in a single database column as a JSON string.
/// </summary>
public class MyCompositeType
{
public int Identifier { get; set; }
public string? Name { get; set; }
}
The type is stored in a single database column, so the registration needs conversion functions for both directions:
using System;
using System.Globalization;
using System.Text.Json;
/// <summary>
/// Conversion functions used when registering <see cref="MyCompositeType"/> as a data type.
/// </summary>
public static class MyCompositeTypeHelper
{
/// <summary>
/// Converts the value to its database representation.
/// </summary>
public static object ConvertToDbRepresentation(MyCompositeType value, object defaultValue, CultureInfo _)
{
if (value == null)
{
return defaultValue;
}
try
{
return JsonSerializer.Serialize(value);
}
catch (Exception e)
{
throw new InvalidOperationException("An error occurred when serializing the data type.", e);
}
}
/// <summary>
/// Converts the value stored in the database to the system representation of the data type.
/// </summary>
public static MyCompositeType ConvertToSystemRepresentation(object value, MyCompositeType defaultValue, CultureInfo _)
{
if (value == null)
{
return defaultValue;
}
// The value is stored in the database as a JSON string
if (value is not string stringValue || string.IsNullOrEmpty(stringValue))
{
return defaultValue;
}
try
{
return JsonSerializer.Deserialize<MyCompositeType>(stringValue) ?? defaultValue;
}
catch (Exception e)
{
throw new InvalidOperationException("An error occurred when deserializing the data type.", e);
}
}
}
Register the type from the OnPreInit method of a module class:
using System.Data;
using CMS;
using CMS.DataEngine;
using CMS.FormEngine;
using CMS.Helpers;
[assembly: RegisterModule(typeof(CustomDataTypeModule))]
/// <summary>
/// Registers the 'mydatatype' custom data type and its code generator during application startup.
/// </summary>
public class CustomDataTypeModule() : Module(nameof(CustomDataTypeModule))
{
/// <summary>
/// String identifier of the data type in the system. Must be lowercase.
/// </summary>
public const string MY_DATA_TYPE = "mydatatype";
// Called during module pre-initialization on application startup
protected override void OnPreInit()
{
base.OnPreInit();
DataTypeManager.RegisterDataTypes(
new DataType<MyCompositeType>(sqlType: "nvarchar(max)", fieldType: MY_DATA_TYPE, schemaType: "xs:string",
conversionFunc: MyCompositeTypeHelper.ConvertToSystemRepresentation,
dbConversionFunc: MyCompositeTypeHelper.ConvertToDbRepresentation,
textSerializer: new DefaultDataTypeTextSerializer(MY_DATA_TYPE))
{
// Stores Unicode values in generated SQL queries. Required for 'nvarchar' columns
SqlValueFormat = DataTypeManager.UNICODE,
DbType = SqlDbType.NVarChar,
// C# type used when generating 'Info' classes for custom module classes
TypeAlias = nameof(MyCompositeType),
DefaultValueCode = "null"
}
);
}
// Code generators must be registered during module initialization
protected override void OnInit()
{
base.OnInit();
// Without a registered code generator, generated content type classes type the field as
// 'object' and include a #warning
DataTypeCodeGenerationManager.RegisterDataTypeCodeGenerator(MY_DATA_TYPE,
() => new DataTypeCodeGenerator(
dataTypeNameRetriever: _ => nameof(MyCompositeType),
validationHelperMethodNameRetriever: _ => $"{nameof(ValidationHelper.GetValue)}<{nameof(MyCompositeType)}>",
defaultValueRetriever: _ => "null",
usingsRetriever: _ => new[] { typeof(MyCompositeType).Namespace! }
));
}
}
The data type is now registered and can be assigned to object fields in the field editor. By default, the field editor offers the data type for the fields of all data classes, but not for reusable field schemas. To control where the data type appears, see the SupportedTargets property in Data type configuration. To let users edit fields based on the data type, you also need to develop a corresponding form component.

Data type configuration
When registering a data type, you can configure properties that control the behavior of the data type in the field editor, code generators, and database queries.
Field editor properties
Hidden– indicates whether the data type is hidden from the field editor.HasConfigurableDefaultValue– indicates whether the data type needs a configurable default value. Controls the visibility of the default value editor and the Required check box in the field editor. See Assign a default value editor.SupportedTargets– selects the kinds of objects whose fields can use the data type. Set to a combination ofDataTypeTargetsflags, joined with the bitwise OR operator. See Data type targets.IsAvailableForDataClass– an additional condition evaluated on top ofSupportedTargets. Use the predicate for restrictions that depend on the individual data class, not only on its kind. See Restrict a data type to specific data classes.
Code generator properties
TypeAlias– the C# type assigned to properties generated for fields of custom object types (Infoclasses). Content type classes use a registered code generator instead. See Register a code generator.DefaultValueCode– C# code for the default value of the data type, provided as a string literal. For example,"String.Empty".CodeValueFormat– the format applied to generated value literals.{0}represents the value. For example, thedecimaldata type uses"{0}m".
General properties
DbType– theSystem.Data.SqlDbTypeused when generating database queries. Set to the same type as thesqlTypeconstructor parameter.DefaultValue– the default value of the data type.SpecialDefaultValues– additional values that the system treats as the default value of the data type.AllowEmpty– indicates whether the underlying database column allows null values. Set totrueby default.SqlValueFormat– the format used when the value is inlined into a generated SQL query.{0}represents the value. The default format wraps the value in single quotes to prevent SQL injection. TheDataTypeManagerclass provides thePLAIN({0}) andUNICODE(N'{0}') constants for the most common cases.StringFormat– the format used when converting values of the data type to a string.{0}represents the value.VariableSize– indicates that the type can have variable size, for examplenvarchar(value). If enabled, the field editor also offers a Size field that lets users set the size of the underlying database column.DefaultSize– the size used when the size isn’t set in the field editor.MaxSize– the largest size accepted for a field. The system rejects fields with a larger size.
VariablePrecision– indicates that the type can have variable precision, for example for floating-point numbers. If enabled, the field editor also offers a Precision field that lets users set the precision of the underlying database column.DefaultPrecision– the precision used when the precision isn’t set in the field editor.MaxPrecision– the largest precision accepted for a field. The system rejects fields with a higher precision.
Data type targets
Set the SupportedTargets property to a combination of the following flags:
|
Flag |
Fields of |
|
|
Reusable content types. |
|
|
Website content types (used by pages). |
|
|
Email content types. |
|
|
Headless content types. |
|
|
|
|
|
System tables, such as OM.Contact. |
|
|
Forms. |
|
|
Customer journey support tables. |
|
|
Data classes that fall into none of the categories above, such as custom module classes. |
|
|
All four content type kinds (reusable, website, email, headless). |
|
|
All data classes – every flag except |
|
|
Every target, including reusable field schemas. |
|
|
No target. The data type is never offered in the field editor. |
Reusable field schemas require an explicit opt-in
The default AllDataClasses value doesn’t include the ReusableFieldSchema flag. Custom data types don’t appear in the field editor of reusable field schemas until you add the flag.
The following example makes the data type available only for the fields of content types and reusable field schemas:
new DataType<MyCompositeType>(sqlType: "nvarchar(max)", fieldType: "mydatatype", schemaType: "xs:string",
conversionFunc: MyCompositeTypeHelper.ConvertToSystemRepresentation,
dbConversionFunc: MyCompositeTypeHelper.ConvertToDbRepresentation,
textSerializer: new DefaultDataTypeTextSerializer("mydatatype"))
{
SupportedTargets = DataTypeTargets.AllContentTypes | DataTypeTargets.ReusableFieldSchema
}
Restrict a data type to specific data classes
The IsAvailableForDataClass predicate narrows the availability of the data type within the kinds of objects allowed by SupportedTargets. The system doesn’t evaluate the predicate for targets without an underlying data class, such as reusable field schemas.
new DataType<MyCompositeType>(sqlType: "nvarchar(max)", fieldType: "mydatatype", schemaType: "xs:string",
conversionFunc: MyCompositeTypeHelper.ConvertToSystemRepresentation,
dbConversionFunc: MyCompositeTypeHelper.ConvertToDbRepresentation,
textSerializer: new DefaultDataTypeTextSerializer("mydatatype"))
{
// Limits the data type to content types
SupportedTargets = DataTypeTargets.AllContentTypes,
// Narrows the availability down to a single content type
IsAvailableForDataClass = dataClassInfo => dataClassInfo.ClassName == "MyProject.Article"
}
Set the display name
The field editor resolves the displayed name of each data type from the base.datatypes.<fieldType> localization resource key, where <fieldType> is the string identifier used during registration. Add the key to a registered resource file:
<data name="base.datatypes.mydatatype" xml:space="preserve">
<value>My composite type</value>
</data>
Because the field editor resolves data type names on the server, register the resource file for the LocalizationTarget.Server target. Without the key, the field editor lists the raw base.datatypes.mydatatype string instead of a readable name. To provide names in other languages, see Admin UI localization.
Register a code generator
Generated code files for content types, reusable field schemas, and other objects need to know which C# type represents each field. The system resolves the type from a DataTypeCodeGenerator registered for the data type. Without one, the generator declares the property as object and adds a warning to the generated file:
#warning Code generator for data type: "mydatatype" is not registered. Create one and register it using DataTypeCodeGenerationManager.RegisterDataTypeCodeGenerator method, in order to generate this property properly.
Register the generator by calling DataTypeCodeGenerationManager.RegisterDataTypeCodeGenerator in the OnInit method of a module class. The method requires all generators to be registered during module initialization. Registration in OnPreInit is not supported.
// Without a registered code generator, generated content type classes type the field as
// 'object' and include a #warning
DataTypeCodeGenerationManager.RegisterDataTypeCodeGenerator(MY_DATA_TYPE,
() => new DataTypeCodeGenerator(
dataTypeNameRetriever: _ => nameof(MyCompositeType),
validationHelperMethodNameRetriever: _ => $"{nameof(ValidationHelper.GetValue)}<{nameof(MyCompositeType)}>",
defaultValueRetriever: _ => "null",
usingsRetriever: _ => new[] { typeof(MyCompositeType).Namespace! }
));
The DataTypeCodeGenerator constructor takes the following functions, each receiving the FormFieldInfo of the generated field:
dataTypeNameRetriever– returns the C# type of the generated property.validationHelperMethodNameRetriever– returns the name of theValidationHelpermethod used to convert the stored value. Include generic parameters where needed, as inGetValue<MyCompositeType>.defaultValueRetriever– returns the C# code for the value assigned when the field is empty.usingsRetriever(optional) – returns the namespaces added asusingdirectives to the generated file. Required whenever the generated property type isn’t in a namespace the file already imports.summaryRetriever(optional) – returns the text of the property’s XML summary comment.
With the generator registered, a field of the custom data type generates as:
using System;
using System.Collections.Generic;
using CMS.ContentEngine;
using Codesamples.Customization.DataTypes;
namespace Codesamples
{
/// <summary>
/// Represents a content item of type <see cref="ProductSKU"/>.
/// </summary>
[RegisterContentTypeMapping(CONTENT_TYPE_NAME)]
public partial class ProductSKU : IContentItemFieldsSource
{
/// <summary>
/// Code name of the content type.
/// </summary>
public const string CONTENT_TYPE_NAME = "Codesamples.ProductSKU";
...
/// <summary>
/// MyCompositeField.
/// </summary>
public MyCompositeType MyCompositeField { get; set; }
}
}
Assign a default value editor
For most data types, the field editor lets users set a default value for each field. For custom data types, assign a suitable UI form component as the default value editor by calling RegisterDefaultValueComponent. The method is available in admin module classes (derived from the AdminModule base class). For more information about admin modules, see Prepare your environment for admin development.
A default value editor is required by default
The HasConfigurableDefaultValue property described in Data type configuration defaults to true. In this state, the data type needs a registered default value editor. The field editor throws an exception when displaying the settings of a field whose data type has no registered component.
Setting HasConfigurableDefaultValue to false has the opposite effect. The field editor hides the default value editor together with the Required check box and never uses a registered component. Register a component only for data types that keep the property enabled. The built-in data types that store references or binary data (Content item asset, Pages and reusable content, Headless items, Smart folder, Taxonomy, and Binary) disable the property instead of providing a default value editor.
The method takes the following parameters:
dataType– the string identifier of the data type.componentIdentifier– the string identifier of the form component used as the default value editor.serializationFunction– aFunc<object, string>that serializes the underlying type of the form component to its string representation.deserializationFunction– aFunc<string, object>that deserializes the string representation back to the underlying type of the form component.
Call the method during admin module initialization:
using CMS;
using CMS.Helpers;
using Kentico.Xperience.Admin.Base;
using Kentico.Xperience.Admin.Base.Forms;
[assembly: RegisterModule(typeof(CustomDataTypeAdminModule))]
/// <summary>
/// Assigns a default value editor to the 'mydatatype' custom data type.
/// </summary>
public class CustomDataTypeAdminModule() : AdminModule(nameof(CustomDataTypeAdminModule))
{
protected override void OnInit()
{
base.OnInit();
// Assigns text input as the default value editor for 'mydatatype'
RegisterDefaultValueComponent(CustomDataTypeModule.MY_DATA_TYPE, TextInputComponent.IDENTIFIER,
ValidationHelper.GetValue<string>, value => ValidationHelper.GetValue<string>(value));
}
}
For a step-by-step implementation of a custom data type, see the following blog post: Embedded structured content and the power of custom data types.