Bender is no longer being maintained. If you are already using it, consider migrating to an actively developed serializer like JSON.NET.
Bender is a highly configurable xml, json, CSV and form url encoded serialization library for .NET. Unlike other serializers, Bender gives you complete control over the serialization process though a simple API.
- Not strongly named.
- One serializer for json, XML, CSV and form url encoded.
- Readers, writers and visitors give you full control of serialization and deserialization.
- Naming conventions give you full control over naming.
- Friendly deserialization error messages allow you to give valuable feedback to consumers of your API.
- Supports enums, nullable types,
String
,Char
,Boolean
,SByte
,Byte
,Int16
,UInt16
,Int32
,UInt32
,Int64
,UInt64
,Single
,Double
,Decimal
,DateTime
,Guid
,TimeSpan
,Uri
,IntPtr
,UIntPtr
,Array
,ArrayList
(Serialization only),IEnumerable
(Serialization only),IEnumerable<T>
,IDictionary
(Serialization only),IDictionary<TKey, TValue>
,byte[]
(As Base64),MailAddress
,Version
,IPAddress
andSqlConnectionStringBuilder
out of the box. - Supports
XmlAttributeAttribute
, andXmlIgnoreAttribute
attributes. - Supports
XmlArrayAttribute
andXmlArrayItemAttribute
attributes, name properties. - Supports
XmlRootAttribute
,XmlTypeAttribute
andXmlElementAttribute
attributes, name and namespace properties. - Extensibility point to override object creation during deserialization with your own implementation.
- Filtering options for types, members and visibility.
- Options for name comparison and ignoring names of certain elements altogether.
- Options for failing on unmatched members or elements.
- Ability to serialize and deserialize members of enumerable and dictionary implementations.
- Ability to transform XML during deserialization.
- Ability to define optional types.
Bender can be found on nuget:
PM> Install-Package Bender
The Serialize
and Deserialize
convenience methods allow you to easily serialize or deserialize:
public class Model
{
public string Property { get; set; }
}
var model = "{\"Property\":\"value\"}".DeserializeJson<Model>();
var model = "<Model><Property>value</Property></Model>".DeserializeXml<Model>();
var json = new Model { Property = "value" }.SerializeJson();
var xml = new Model { Property = "value" }.SerializeXml();
Additional convenience methods take or return a byte[]
, Stream
, file path, XDocument
, XElement
and INode
.
Bender can be configured in a number of different ways depending on your needs. First, the configuration DSL is available on all convenience and factory methods:
var model = "{ ... }".DeserializeJson<Model>(o => { ... });
var serializer = Serializer.Create(o => { ... });
var deserializer = Deserializer.Create(o => { ... });
Second, you can create options with the options factory method and pass those in later:
var options = Options.Create(o => { ... });
var model = "{ ... }".DeserializeJson<Model>(options);
var serializer = new Serializer(options);
var deserializer = new Deserializer(options);
Method | Description |
---|---|
WithGenericTypeNameFormat |
Naming convention for generic types. The default is {0}Of{1} . The first parameter is the type name and the second is the type parameter(s). |
WithEnumerableTypeNameFormat |
Naming convention for enumerable types. The default is ArrayOf{0} . The parameter is the item type name. |
WithDictionaryTypeNameFormat |
Naming convention for dictionaries. The default is ``DictionaryOf{0}. The parameter is the value type name. |
WithDefaultItemTypeName |
Naming convention for non generic enumerable and dictionary type items. The default is AnyType . |
TreatEnumerableImplsAsObjects |
Indicates whether types implementing IEnumerable are treated as objects, where members are traversed, or enumerables where items are traversed. |
TreatDictionaryImplsAsObjects |
Indicates whether types implementing IDictionary or IDictionary<TKey, TValue> are treated as objects, where members are traversed, or dictionaries where key/value pairs are traversed. |
IncludeNonPublicProperties |
Indicates that non public properties should also be traversed. By default, non public properties are ignored. |
IncludePublicFields |
Indicates that public fields should also be traversed. By default, fields are ignored. |
IncludeNonPublicFields |
Indicates that non public fields should also be traversed. By default, fields are ignored. |
IncludeTypesWhen |
Predicate that determines what types should be included. Multiple predicates can be specified and are additive. By default all types are included. |
ExcludeTypesWhen |
Predicate that determines what types should be excluded. Multiple predicates can be specified and are additive. |
ExcludeType<T> |
Specifies a type that should be excluded. Multiple types can be specified and are additive. |
IncludeMembersWhen |
Predicate that determines what properties and fields should be included. Multiple predicates can be specified and are additive. By default all public properties are included. |
ExcludeMembersWhen |
Predicate that determines what properties and fields should be excluded. Multiple predicates can be specified and are additive. |
UseSnakeCaseNaming |
Deserializes from and serializes to snake_case . |
UseCamelCaseNaming |
Deserializes from and serializes to camelCase . |
UseJsonCamelCaseNaming |
Deserializes from and serializes to camelCase only for json. |
UseXmlSpinalCaseNaming |
Deserializes from and serializes to spinal-case only for xml. |
UseXmlTrainCaseNaming |
Deserializes from and serializes to Train-Case only for xml. |
WithMemberNamingConvention |
Function that returns the name of a field or property. Multiple conventions can be added to form a pipeline. |
WithFieldNamingConvention |
Function that returns the name of a field. Multiple conventions can be added to form a pipeline. |
WithPropertyNamingConvention |
Function that returns the name of a property. Multiple conventions can be added to form a pipeline. |
WithArrayItemNamingConvention |
Function that returns the name of an array item. Multiple conventions can be added to form a pipeline. |
WithTypeNamingConvention |
Function that returns the name of a type. Multiple conventions can be added to form a pipeline. |
WithNamingConvention |
Function that returns the name of fields, properties, array items and types. Multiple conventions can be added to form a pipeline. |
Method | Description |
---|---|
PrettyPrint |
Includes whitespace in the output. |
IncludeNullMembers |
Serializes null members. By default these are ignored. |
UseActualType |
Indicates whether to serialize the actual type as opposed to the cast type. The cast type is used by default. |
WriteDateTimeAsUtcIso8601 |
Serialize DateTime 's as ISO8601. |
WriteMicrosoftJsonDateTime |
Serialize DateTime 's as the Microsoft date format for json only (e.g. /Date(1330848000000)/ ). |
XmlValuesAsAttributes |
Serialize values as XML attributes instead of elements. |
WithDefaultXmlNamespace |
Allows you to specify the default XML namespace. |
AddXmlNamespace |
Allows you to add an XML namespace. |
OmitXmlDeclaration |
Indicates that you want to omit the XML declaration. |
AsSimpleType<T> |
Treat an object as a simple type, calling ToString() for the value. |
AddVisitor |
Adds node visitor. |
AddJsonVisitor |
Adds node visitor for json only. |
AddXmlVisitor |
Adds node visitor for xml only. |
AddVisitor<T> |
Adds node visitor for the specified type. |
AddJsonVisitor<T> |
Adds node visitor for the specified type for json only. |
AddXmlVisitor<T> |
Adds node visitor for the specified type for xml only. |
AddWriter |
Adds a writer. |
AddWriter<T> |
Adds a writer for the specified type. |
Method | Description |
---|---|
WithObjectFactory |
Allows you to specify a factory for creating objects. Useful if you would like to hydrate objects with an IoC container. |
IgnoreNameCase |
Ignores name casing. |
WithNameComparison |
Allows you to set the specific StringComparison for name matching. |
IgnoreEnumNameCase |
Ignores enum value casing. |
WithEnumNameComparison |
Allows you to set the specific StringComparison for enum value matching. |
IgnoreXmlAttributes |
Ignores XML attributes. |
IgnoreRootName |
Does not attempt to match the root element name. |
IgnoreUnmatchedArrayItems |
Ignores array items whose names do not match. This throws an exception by default. |
IgnoreArrayItemNames |
Does not attempt to match array item element names. |
IgnoreNullsForValueTypes |
Ignores null source values when the target is a value type (which would otherwise cause an exception). |
IgnoreEmptyCsvValues |
Ignore empty CSV values (e.g. fields with nothing or only whitespace between the separators: 1,,3,4 ). |
FailOnUnmatchedElements |
Fail if an element exists but not it's corresponding member. |
FailOnUnmatchedMembers |
Fail if a member exists but not it's corresponding element. |
TreatDatesAsUtcAndConvertToLocal |
Treat dates as local but serialize as UTC. |
WithFriendlyParseErrorMessage<T> |
Allows you to specify friendly parse error messages for specific types. |
AddVisitor |
Adds node visitor. |
AddJsonVisitor |
Adds node visitor for json only. |
AddXmlVisitor |
Adds node visitor for xml only. |
AddVisitor<T> |
Adds node visitor for the specified type. |
AddJsonVisitor<T> |
Adds node visitor for the specified type for json only. |
AddXmlVisitor<T> |
Adds node visitor for the specified type for xml only. |
AddReader |
Adds a reader. |
AddReader<T> |
Adds a reader for the specified type. |
Bender has a number of extensibility points. The following covers these in more detail.
Bender supports a number of attributes that override serialization and deserialization.
Attribute | Format | Description |
---|---|---|
XmlIgnoreAttribute |
All | Indicates that a member should not be serialized or deserialized. |
XmlArrayAttribute |
All | Overrides the name of an enumerable member. |
XmlElementAttribute |
All | Overrides the name of a member. |
XmlRootAttribute |
XML | Overrides the name of the root element. |
XmlTypeAttribute |
XML | Overrides the name of the root element or an enumerable item. |
XmlAttributeAttribute |
XML | Indicates that a member should be serialized as an XML attribute and optionally overrides it's name. |
XmlArrayItemAttribute |
XML | Overrides the name of an enumerable item. |
XmlSiblingsAttribute |
XML | Serializes or deserializes an enumerable item as a sibling of other members in the type. |
WithAttributeAttribute |
XML | Adds an attribute to an element. |
Bender ships with some common naming conventions out of the box: camelCase
, snake_case
, spinal-case
and Train-Case
. You can add your own naming conventions with the With*NamingConvention()
options. There are options to override naming for fields, properties, members (fields and properties), array items, types or all of the above. Each option method has an overload that applies to all and one that takes a predicate to limit its application. Naming conventions form a pipeline where a name is modified by one convention and then passed to the next convention to be modified further. This allows you to define multiple conventions that work together. The order that the conventions are defined is the order they will execute.
Lets take a look at an arbitrary example where we create a couple of naming conventions.
Options.Create(o => o
.WithPropertyNamingConvention(
(name, member) => name + "EmailAddress",
(name, member) => member.Type == typeof(MailAddress))
.WithNamingConvention(name => "_" + name));
In the first convention we append EmailAddress
to the name when the property is of type MailAddress
. Next we define a convention that prepends an _
to every name. If we had a property named Support
of type MailAddress
the name would end up being _SupportEmailAddress
.
Bender also allows you to define naming conventions for enum values. Enum values are not included in the global naming convention (As demonstrated above) as it is assumed that they will differ from type and member naming conventions. Below we see the convenience method for snake casing and it's actual implementation.
Options.Create(o => o.UseEnumSnakeCaseNaming());
Options.Create(o => o
.WithEnumNamingConvention(
(value, context) => value.Replace("_", ""),
(value, context) => context.Mode == Mode.Deserialize)
.WithEnumNamingConvention(
(value, context) => value.ToSeperatedCase(false, "_"),
(value, context) => context.Mode == Mode.Serialize);
Visitors enable you to operate on each node in the target graph. Both serialization and deserialization options contain methods for adding visitors. Each option method has an overload that applies to all and one that takes a predicate to limit its application. There are also convenience methods that apply visitors to either xml or json or specific types.
Lets take a look at an xml visitor for serialization that adds a nil
attribute when a value is null.
Options.Create(o => o
.Serialization(s => s
.AddXmlNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
.AddXmlVisitor(
(source, target, options) =>
target.Element.Add(new XAttribute("xsi:nil", "true")),
(source, target, options) => source.NodeType.IsValue() &&
source.Value == null)));
Readers read values from a source like xml or json during deserialization. Each option method has an overload that applies to all and one that takes a predicate to limit its application. There are also convenience methods that apply readers to specific types.
Lets look at an example where we read an IP address.
Options.Create(o => o
.Deserialization(d => d
.AddReader(value => IPAddress.Parse(value.ToString()))));
You can also define readers for nullable and non-nullable types separately if you want to have fine grained control, for example if you wanted to override how bool
's are parsed:
Options.Create(o => o
.Deserialization(d => d
.AddReader<bool>(value => bool.Parse(value.ToString()))
.AddReader<bool?>(value =>
!string.IsNullOrEmpty(value.ToString()) ?
(bool?)bool.Parse(value.ToString()) : null)));
But most of the time the functionality will be the same for nullable and non nullable readers and writers, save the boilerplate null checking logic. So you can define one writer for both nullable and non-nullable types by passing true to the handleNullable
parameter:
Options.Create(o => o
.Deserialization(d => d
.AddReader<bool>(value => bool.Parse(value.ToString()), true)));
Writers write values to a target like xml or json during serialization. Each option method has an overload that applies to all and one that takes a predicate to limit its application. There are also convenience methods that apply readers to specific types and indicate if nullable types are included.
Lets look at an example where we write an IP address.
Options.Create(o => o
.Serialization(s => s
.AddWriter<IPAddress>(value => value.ToString())));
Sometimes when deserializing you may want to know when source values were set. For example, an API that will only update values that were passed. Or you may only want to serialize values that have been set. One approach is to ignore all null values, using nullable value types. The problem with this is that null might actually be a valid value. To address this issue, Bender supports optional types. Optional types are akin to nullable types, with HasValue
and Value
properties. They can also be implicitly and explicitly cast. Lets say we have an API that lets users set a termination date, among other things, which can be null. The json will be deserialized to the following model:
public class EmployeeModel
{
public Optional<string> Name { get; set; }
public Optional<DateTime?> TerminationDate { get; set; }
...
}
And lets say the user passes the following json to our API:
{
"terminationDate": null
}
The model can then be mapped appropriately since we know what values were passed:
if (employeeModel.Name.HasValue)
employeeEntity.Name = employeeModel.Name;
if (employeeModel.TerminationDate.HasValue)
employeeEntity.TerminationDate = employeeModel.TerminationDate;
This will ignore the name field as it was not passed but set the termination date to null.
The opposite occurs on serialization. For example lets say we return the model above, with only the termination date set, from our API:
new EmployeeModel {
TerminationDate = null
}
This would produce the following json:
{
"terminationDate": null
}
All other properties on the model will not be serialized as they were not set. Note, the IncludeNullMembers()
configuration option must be used to serialize null values.
Errors during deserialization can result from issues with the source or with your code and configuration. In an API consumed by others, the former can likely be addressed by your end users if they have good feedback. With that in mind, all Bender deserialization exceptions that are caused by a problem with the source inherit from FriendlyBenderException
. This exception has a property named FriendlyMessage
that can be displayed to end users and give them information to help them resolve the problem. Unlike the raw error messages, no internal information is included.
Bender also has default friendly messages for parsing simple types and these can be overridden by calling the WithFriendlyParseErrorMessage<T>(string message)
method in the deserialization options. Also, when creating readers you can make use of friendly parse error messages by specifying one for the type your reader handles:
var deserializer = Deserializer.Create(o => o
.Deserialization(d => d
.WithFriendlyParseErrorMessage<IPAddress>(
"IP addresss not formatted correctly, must be formatted as '1.2.3.4'.")
.AddReader(value => IPAddress.Parse(value.ToString()))));
Any errors resulting from the reader will be wrapped in a FriendlyBenderException
with the friendly error specified.
Bender has traded some performance for flexibility so does not match the speed of other serializers. It averages ~.05 ms/KB (2.8 GHz i7) so would be more than enough for most applications. But if raw speed is a requirement you might want to look at the ServiceStack serializers or fastJSON.