---
title: Serialization
slug: cms-12/api/serialization
docTags: 
createdAt: 2026-08-07T16:45:42.000Z
---

:::hint{type="info"}
**Note**
This section applies only if you are still using Content Delivery Api v2.x.x. It does not apply to customers using version 3.
:::

## What is serialization?

The Content Delivery API takes your content in the Optimizely database and converts it into JSON format, which encodes objects in a string. This conversion of objects into a string is called **serialization**. The opposite conversion, from string to object, is called deserialization.

Serialization converts complex objects into byte strings, which can be transmitted. After the transmission, the recipient has to deserialize the byte strings to recover the original object.

:::hint{type="info"}
**Example**
If you have the object `{foo: [1, 4, 7, 10], bar: "epi"}`, serializing this into JSON  converts it into the string `'{"foo":[1,4,7,10],"bar":"epi"}'` . The recipient can then deserialize this string into the original object `{foo: [1, 4, 7, 10], bar: "epi"}`.
:::

## How it works

The Content Delivery API defines two concepts, `ContentApiModel` and `PropertyModel`, which plays a role as data transform objects for the serialization process. Data is transformed to those models before being returned as the requested result. The main serialization flow is illustrated by the diagram below.

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/Rfz49DdT-VDU4XapkT-oV_8ef5a30-1serialization.jpg)

The workflow should be as follows:

1. You request an endpoint to query data.
2. The request is handled by components (filter, controllers, services, and so on) and assumes that the expected data is retrieved in the forms of `IContent`, `IEnumerable<IContent>`, or `Errors`.

`IContent` instances are transformed to `ContentApiModel` so this is a copy of the content populated with whatever content interfaces your content implements.

- The properties of `IContent` are migrated to `ContentApiModel`.
- The `PropertyDataCollection` of `IContent` is converted to a list of `PropertyModel` and then migrated to `ContentApiModel`.
- `ContentApiModel` is wrapped in `ContentApiResult`. Then, the serializer of `ContentDeliveryApi` serializes everything into JSON and returns it.

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/D8gP_GtBXJ9JHqZ7oUMwh_86db7d5-2serialization.png)

This table maps properties between `ContentData` and `ContentApiModel`:

| ` ContentApiModel`                              | ` PageData`                                         |
| ----------------------------------------------- | --------------------------------------------------- |
| ` ContentLink : ContentModelReference`          | ` ContentLink : ContentReference`                   |
| ` Name` :   *string*                            | ` Name` :  *string*                                 |
| ` ParentLink` :  *ContentModelReference*        | ` ParentLink` :  *ContentReference*                 |
| ` ContentType` :  *List*`<string>`              | ` ContentTypeID` : *int*                            |
| ` Language` :  *LanguageModel*                  | ` Language` :  *CultureInfo*                        |
| ` ExistingLanguages` :  *List*`<LanguageModel>` | ` ExistingLanguage` :  *IEnumerable*`<CultureInfo>` |
| ` MasterLanguage` : *LanguageModel*             | ` MasterLanguage` :  *CultureInfo*                  |
| ` RouteSegment` :  *string*                     | ` URLSegment` :  *string*                           |
| ` Url` :  *string*                              | ` LinkURL` :  *string*                              |
| ` Changed` :  *DateTime?*                       | ` Changed` :  *DateTime*                            |
| ` Created` : *DateTime?*                        | ` Created` : *DateTime*                             |
| ` StartPublish` :  *DateTime?*                  | ` StartPublish` :  *DateTime?*                      |
| ` StopPublish` :  *DateTime?*                   | ` StopPublish` : *DateTime?*                        |
| ` Saved` : *DateTime?*                          | ` Saved` :  *DateTime*                              |
| ` Status` :  *string*                           | ` Status` :   *VersionStatus*                       |
| ` Properties` :  *IDictionary\<string,object>*  | `Property: `  *PropertyDataCollection*              |

Example  –   Serialization, Alloy template

You can use the Alloy template to see the default configuration with predefined endpoints and output models.

## ProductPage class

This is the code for the Alloy `ProductPage`:

```csharp
namespace EpiAlloy.Models.Pages {
  /// <summary>
  /// Used to present a single product
  /// </summary>
  [SiteContentType(
    GUID = "17583DCD-3C11-49DD-A66D-0DEF0DD601FC",
    GroupName = Global.GroupNames.Products)]
  [SiteImageUrl(Global.StaticGraphicsFolderPath + "page-type-thumbnail-product.png")]
  [AvailableContentTypes(
    Availability = Availability.Specific,
    IncludeOn = new [] {
      typeof (StartPage)
    })]
  public class ProductPage: StandardPage, IHasRelatedContent {
    [Required]
    [Display(Order = 305)]
    [UIHint(Global.SiteUIHints.StringsCollection)]
    [CultureSpecific]
    public virtual IList<string> UniqueSellingPoints {
      get;
      set;
    }

    [Display(
      GroupName = SystemTabNames.Content,
      Order = 330)]
    [CultureSpecific]
    [AllowedTypes(new [] {
      typeof (IContentData)
    }, new [] {
      typeof (JumbotronBlock)
    })]
    public virtual ContentArea RelatedContentArea {
      get;
      set;
    }
  }
}
```

If you retrieve a product page through the Content Delivery API, you will get the following result:

JSON output from Alloy product page

```json
{
  "contentLink": {
    "id": 6,
    "workId": 0,
    "guidValue": "567a6012-5af2-4f26-a198-593326b80722",
    "providerName": null,
    "url": "/en/alloy-plan/"
  },
  "name": "Alloy Plan",
  "language": {
    "link": "/en/alloy-plan/",
    "displayName": "English",
    "name": "en"
  },
  "existingLanguages": [
    {
      "link": "/en/alloy-plan/",
      "displayName": "English",
      "name": "en"
    }
  ],
  "masterLanguage": {
    "link": "/en/alloy-plan/",
    "displayName": "English",
    "name": "en"
  },
  "contentType": [
    "Page",
    "ProductPage"
  ],
  "parentLink": {
    "id": 5,
    "workId": 0,
    "guidValue": "0b9c1a6e-129a-456b-ac3c-5a1b108362e0",
    "providerName": null,
    "url": "/en/"
  },
  "routeSegment": "alloy-plan",
  "url": "/en/alloy-plan/",
  "changed": "2019-10-28T14:26:13Z",
  "created": "2012-08-22T15:15:48Z",
  "startPublish": "2012-08-22T15:15:48Z",
  "stopPublish": null,
  "saved": "2019-10-28T14:26:13Z",
  "status": "Published",
  "category": {
    "value": [
      {
        "id": 3,
        "name": "Plan",
        "description": "Alloy Plan"
      }
    ],
    "propertyDataType": "PropertyCategory"
  },
  "metaTitle": {
    "value": "Alloy Plan, online project management",
    "propertyDataType": "PropertyLongString"
  },
  "pageImage": {
    "value": {
      "id": 43,
      "workId": 0,
      "guidValue": "c04c11c5-4314-4fdc-b1b8-c3bfb9fdb9d8",
      "providerName": null,
      "url": null
    },
    "propertyDataType": "PropertyContentReference"
  },
  "teaserText": {
    "value": "Project management has never been easier!",
    "propertyDataType": "PropertyLongString"
  },
  "hideSiteHeader": {
    "value": null,
    "propertyDataType": "PropertyBoolean"
  },
  "metaDescription": {
    "value": "Project management has never been easier! Use Alloy Meet with Alloy Plan to get the whole team involved in the creation of project plans and see how this commitment translates into finite and achievable goals.",
    "propertyDataType": "PropertyLongString"
  },
  "hideSiteFooter": {
    "value": null,
    "propertyDataType": "PropertyBoolean"
  },
  "uniqueSellingPoints": {
    "value": [
      "Project planning",
      "Reporting and statistics",
      "Email handling of tasks",
      "Risk calculations",
      "Direct communication to members"
    ],
    "propertyDataType": "PropertyStringList"
  },
  "mainBody": {
    "value": "<p><img style=\"float: left;\" src=\"/contentassets/89bccbae16d14665b08fac3525c9a999/alloyplanscreen.png\" alt=\"Alloy Plan - Efficient project planning\" /></p>\n<p>Planning is crucial to the success of any project. Alloy Plan takes into consideration all aspects of project planning; from well-defined objectives to staffing, capital investments and management support. Nothing is left to chance.</p>\n<p>Alloy Plan supports all project methodologies efficiently as the system is totally flexible in terms of setup and use.</p>\n<p>Realize the benefits of using Alloy Plan. Our customers see on average  an 80% increase in delivery of their projects on time, on budget and  with minimal risk involved.</p>\n<p>Work with an Alloy Technology partner to define the scale of your organization's needs and find the best fit with Alloy Plan.</p>",
    "propertyDataType": "PropertyXhtmlString"
  },
  "mainContentArea": {
    "value": [
      {
        "displayOption": "wide",
        "tag": null,
        "contentLink": {
          "id": 46,
          "workId": 0,
          "guidValue": "7026878a-a6e3-4916-811d-40bf3fd9b50b",
          "providerName": null,
          "url": null
        }
      },
      {
        "displayOption": "narrow",
        "tag": null,
        "contentLink": {
          "id": 28,
          "workId": 0,
          "guidValue": "2e90d62d-4abe-4c75-b87e-27817edad095",
          "providerName": null,
          "url": null
        }
      },
      {
        "displayOption": "narrow",
        "tag": null,
        "contentLink": {
          "id": 31,
          "workId": 0,
          "guidValue": "da738746-4912-4603-953c-d727f744fa91",
          "providerName": null,
          "url": null
        }
      }
    ],
    "propertyDataType": "PropertyContentArea"
  },
  "relatedContentArea": {
    "value": [
      {
        "displayOption": "",
        "tag": null,
        "contentLink": {
          "id": 47,
          "workId": 0,
          "guidValue": "2dfadba3-31f8-45ac-9d80-6c8ff3a51b5e",
          "providerName": null,
          "url": null
        }
      },
      {
        "displayOption": "",
        "tag": null,
        "contentLink": {
          "id": 48,
          "workId": 0,
          "guidValue": "87d2fa31-712a-4dac-b57c-a369d28f149b",
          "providerName": null,
          "url": null
        }
      }
    ],
    "propertyDataType": "PropertyContentArea"
  },
  "disableIndexing": {
    "value": null,
    "propertyDataType": "PropertyBoolean"
  }
}
```

As you can see, you get a lot of data with the default configuration. However, all this data, such as `masterLanguage`, `changed`, `created`, `startPublish`, `stopPublish`, might not be necessary for your front-end site. Other pieces might be necessary but in another format; `contentLink` and `parentLink` should perhaps be included but as integer IDs, and perhaps you would like enum rendered as a string name and not as a number, and URLs to be relative and not absolute. This is where some customization of the serialization needs to be done.

## Customize the serialization

If the standard JSON output does not suit your needs, you can customize the serialization for a different output. You can customize the serialization in several ways:

- Using the `JsonIgnore` attribute
- Property Model mappers
- Customize the `IContentModelMapper` implementation
- Filter the output model
- Create custom controller

### JsonIgnore attribute

If you have `ContentData` properties that you do not want to be included in the JSON output, just decorate it with the `JsonIgnore` attribute and it is ignored in the resulting model. The attribute is checked by the `ContentModelMapperBas`e during `ContentApiModel` building and ignores it if it is present.

```csharp
[JsonIgnore]
	[Display(
		GroupName = SystemTabNames.Content,
		Order = 330)]
	[CultureSpecific]
	[AllowedTypes(new[] { typeof(IContentData) },new[] { typeof(JumbotronBlock) })]
	public virtual ContentArea RelatedContentArea { get; set; }
```

### Property model mappers

The `Property` model is the Content Delivery version of `PropertyData`, so this is the JSON rendered for your content properties.

For example, the `PropertyNumber` is represented by `NumberPropertyModel`. The `NumberPropertyModel` has a `Value` (the numeric value of property) and a `PropertyDataType` which usually is the name of the value type (in this case `PropertyNumber`).

```csharp
/// <summary>
/// Mapped property model for <see cref="PropertyNumber"/>
/// </summary>
public class NumberPropertyModel: PropertyModel<int?, PropertyNumber> {
  public NumberPropertyModel(PropertyNumber propertyNumber): base(propertyNumber) {
    Value = propertyNumber.Number;
  }
}
```

These are the base classes or interfaces for model serialization.

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/sGFTVKblv1kpo2giIwQzG_1b503bd-3serialization.png)

You can use these base classes to implement and register your own **custom property models** and that way, change how custom property types from `PageData` are serialized. See Custom property models for information.

### IPropertyModel

This is the most basic Interface that needs to be implemented according to the default `PropertyModelConverter` (see below). It only contains two properties:

| **Type**   | **Name**         | **Description**                                                                      |
| ---------- | ---------------- | ------------------------------------------------------------------------------------ |
| **string** | Name             | The name of Content Api Property Model, usually the name of Optimizely Property type |
| **string** | PropertyDataType | The name of the Optimizely PropertyData                                              |

### PropertyModel\<TValue,TType>

The `IPropertyModel` interface also needs a constructor with a parameter with the `PropertyData`. For convenience, the Content Delivery API has a base class for this, that is `EPiServer.ContentApi.Core.Serialization.Models.PropertyModel\<TValue,TType>`, where TValue is the value that should be outputted in JSON and TType is the type of `PropertyData`.

As the `PropertyModel` is serialized to JSON, all properties on the implementation are included in the result. The only exception is if you have decorated a property with the `JsonIgnore` attribute.

### IPersonalizableProperty

If the output of the property depends on personalization, implement the `IPersonalizableProperty`. The interface only requires a Property called `ExcludePersonalizedContent` which determines if personalized content should be excluded when retrieving content.

```csharp
public class MyPersonalizedPropertyModel: IPropertyModel, IPersonizableProperty {
  public MyPersonalizedPropertyModel(MyPropertyData property, bool excludePersonalizedContent) {
    if (excludePersonalizedContent) {
      // add logic to exclude personalized content
    }
  }
  public string Name {
    get;
    set;
  }
  public string PropertyDataType {
    get;
    set;
  }
  public bool ExcludePersonalizedContent {
    get;
    set;
  }
}
```

By default, property models of `ContentArea`, `ContentReference`, `ContentReferenceList`, and `LinkCollection` implement this interface. Content Delivery API also has a base class that is `EPiServer.ContentApi.Core.Serialization.Models.PersonalizablePropertyModel\<TValue, TType>`  where TValue is the value that should be outputted in your JSON, and TType is the type of `PropertyData`.

### IExpandableProperty

This interface is used for properties where you can return a simplified data set for the initial API Request so that the server can make further requests to the database to dig deeper into the data. By default, the following property types can be expanded: `ContentArea`, `ContentReference`, `ContentReferenceList`, and `LinkCollection`.

Expand it by adding the parameter `?expand=Steps` or `?expand=\*`. Note that expanding a property only works on the first level of nested content but not on lower levels.

Code example: Response without expanded parameter

```json
{
  "MyContentArea": {
    "Value": [
      {
        "ContentLink": {
          "Id": 9,
          "WorkId": 0,
          "GuidValue": "5f3d81e6-28f8-4f16-b998-87378fc9c4d6",
          "ProviderName": null
        },
        "DisplayOption": null,
        "Tag": null
      }
    ],
    "PropertyDataType": "PropertyContentArea"
  }
}
```

Code example: Response with expanded parameter

```json
{
  "MyContentArea": {
    "ExpandedValue": [
      {
        "ContentLink": {
          "Id": 9,
          "WorkId": 0,
          "GuidValue": "5f3d81e6-28f8-4f16-b998-87378fc9c4d6",
          "ProviderName": null
        },
        "Name": "My Block",
        "Language": {
          "DisplayName": "English",
          "Name": "en"
        },
        "ExistingLanguages": [
          {
            "DisplayName": "English",
            "Name": "en"
          }
        ],
        "MasterLanguage": {
          "DisplayName": "English",
          "Name": "en"
        },
        "ContentType": [
          "Block",
          "MyBlock"
        ],
        "ParentLink": {
          "Id": 8,
          "WorkId": 0,
          "GuidValue": "87a9ae8a-a2a8-40e5-8c09-5c5c55a73e17",
          "ProviderName": null
        },
        "RouteSegment": null,
        "Url": null,
        "Changed": "2018-04-09T16:05:01Z",
        "Created": "2018-04-09T16:05:01Z",
        "StartPublish": "2018-04-09T16:05:01Z",
        "StopPublish": null,
        "Saved": "2018-04-09T16:05:34Z",
        "Status": "Published",
        "Category": {
          "Value": [],
          "PropertyDataType": "PropertyCategory"
        }
      }
    ],
    "Value": [
      {
        "ContentLink": {
          "Id": 9,
          "WorkId": 0,
          "GuidValue": "5f3d81e6-28f8-4f16-b998-87378fc9c4d6",
          "ProviderName": null
        },
        "DisplayOption": "",
        "Tag": null
      }
    ],
    "PropertyDataType": "PropertyContentArea"
  }
}
```

### IPropertyModelConverter

This interface is responsible for mapping data between `PropertyData` and `PropertyModel`. The default implementation  `DefaultPropertyModelConverter` uses reflection to map `PropertyData` with the corresponding `PropertyModel`.

## Custom property models

If you do not want to use the built-in property models, you can implement and register custom property models. These can change how a property type is serialized and apply to custom property types of the page data. The custom property models do not affect the internal `IContent` object properties or the visibility of the property.

To create a custom property model, create a class that inherits from `EPiServer.ContentApi.Core.Serialization.Models.PropertyModel` and set the `Value` property in your constructor. You can map your property type to any class if it is serializable and properly indexed into Optimizely Search & Navigation (formerly Optimizely Find).

Example: Forcing `PropertyLongString` to lowercase

The following custom property model forces all `PropertyLongString` instances to lowercase.

```csharp
public class LowercaseLongStringPropertyModel: PropertyModel<string, PropertyLongString> {
  public LowercaseLongStringPropertyModel(PropertyLongString propertyLongString): base(propertyLongString) {
    if (propertyLongString != null) {
      Value = propertyLongString.ToString().ToLower();
    }
  }
}
```

Example: Changing the Boolean value to an integer

This example changes the rendering of Boolean values null, true, and false to integers -1, 1, 0:

```csharp
public class CustomBooleanPropertyModel: PropertyModel< int, PropertyBoolean> {
  public CustomBooleanPropertyModel(PropertyBoolean propertyBoolean): base(propertyBoolean) {
    this.Value = propertyBoolean.Boolean.HasValue ? Convert.ToInt32(propertyBoolean.Boolean.Value) : -1;
  }
}
```

Example: Include local blocks in JSON (for versions 2.6.1 and lower)

In versions before 2.9, the JSON output did not include local blocks by default. To include those, you had to create a custom property model and put `PropertyBlock` as TType.

1. Create a custom block. [Music Festival code example](https://github.com/episerver/musicfestival-vue-template/blob/master/src/MusicFestival.Vue.Template/Models/Blocks/BuyTicketBlock.cs).
2. Create a property model for the custom block. [Music Festival code example](https://github.com/episerver/musicfestival-vue-template/blob/master/src/MusicFestival.Vue.Template/Infrastructure/BuyTicketBlockPropertyModel.cs).
3. Create a property model converter. [Music Festival code example](https://github.com/episerver/musicfestival-vue-template/blob/master/src/MusicFestival.Vue.Template/Infrastructure/BuyTicketBlockPropertyModelConverter.cs).
4. Display its value on the user interface. [Music Festival code example](https://github.com/episerver/musicfestival-vue-template/blob/master/src/MusicFestival.Vue.Template/Assets/Scripts/components/blocks/BuyTicketBlock.vue).

Along with `EPiServer.ContentApi.Core.Serialization.Models.PropertyModel` custom property models can also inherit from `EPiServer.ContentApi.Core.Serialization.Models.PersonalizablePropertyModel`. This class is used when custom property models contain data dependent on personalization. It adds a constructor parameter, `excludePersonalizedContent`, which lets you implement logic in your property model to set the Value property differently in personalized versus non-personalized contexts.

### Register custom property models

For your custom property models to take effect, you need to create a custom implementation of `IPropertyModelConverter`. The implementation of `IPropertyModelConverter` has a collection of `EPiServer.ContentApi.Core.Serialization.Models.TypeModel` called `ModelTypes`, which maintains a map of which.`PropertyData` instances that the converter is capable of handling.

The simplest method of adding a custom converter is to extend the default one, `DefaultPropertyModelConverter`, and override the `SortOrder` and `ModelTypes` properties.

**Example**: The following handler registers the `LowercaseLongStringPropertyModel` from the above example, with a `SortOrder` higher than the default handler `(SortOrder = 0)`, ensuring the custom converter is used for Long String properties.

```csharp
[ServiceConfiguration(typeof (IPropertyModelConverter), Lifecycle = ServiceInstanceScope.Singleton)]
public class LowercaseLongStringPropertyModelConverter: DefaultPropertyModelConverter {
  public LowercaseLongStringPropertyModelConverter() {
    ModelTypes = new List<TypeModel> {
      new TypeModel {
        ModelType = typeof (LowercaseLongStringPropertyModel), ModelTypeString = nameof(LowercaseLongStringPropertyModel), PropertyType = typeof (PropertyLongString)
      }
    };
  }
  public override int SortOrder {
    get;
  } = 100;
}
```

In some cases, a full implementation of `IPropertyModelConverter` may be preferable, such as when custom logic is required to choose between different `PropertyModel` implementations. In that case, your custom converter must also implement `HasPropertyModelAssociatedWith`, which verifies, based on the provided `PropertyData`, that the implementation of `IPropertyModelConverter`can handle the provided type. In addition, your custom converter must also implement `ConvertToPropertyModel` method, which creates and returns any instances of your custom property models based on the provided instance of `PropertyData`.

## Work with ContentModelMapper

The `IContentModelMapper` implementation is where you can customize built-in PageData properties. It gives you full control over `ContentApiModel` and its properties are converted and mapped. Derive from `DefaultContentModelMapper` and then override one or more virtual methods of `ContentModelMapperBase`.

Note that the resulting objects are of the `ContentApiModel` type so that you may have unnecessary data in your resulting output. See Output model filtering.

See the [MusicFestival template site](https://github.com/episerver/musicfestival-vue-template/blob/master/src/MusicFestival.Vue.Template/Models/ExtendedContentModelMapper.cs) for some examples of extending `ContentModelMapper`.

## Output model filtering

By customizing `ContentResultService`, you can filter out properties from the data returned by Content Delivery API. This is called **model filtering** and has the benefit of decreasing your models. See [How to customize API to change data returned to clients](docId\:G0-aJ6fe4B2u8u7UHsx1L) for information.

## Create a custom controller

You can create and reuse your custom controller for different scenarios if you have special requirements.

See also:  [Customized Transformation of IContent for ContentDeliveryApi](https://world.optimizely.com/blogs/vegard-solheim/dates/2020/5/simple-customized-transformation-of-icontent-for-contentdeliveryapi/) by Vegard Solheim
