Skip to content

Support extending NEST types by deriving and implementing properties #3655

@russcam

Description

@russcam

With the move to utf8json in #3493, the serialization of NEST types is strict, serializing only those properties specified on implemented interfaces in many places. For example, PropertyFormatter only serializes the I*Property interface members:

public void Serialize(ref JsonWriter writer, IProperty value, IJsonFormatterResolver formatterResolver)
{
if (value == null)
{
writer.WriteNull();
return;
}
switch (value.Type)
{
case "text":
Serialize<ITextProperty>(ref writer, value, formatterResolver);
break;
case "keyword":
Serialize<IKeywordProperty>(ref writer, value, formatterResolver);
break;
case "float":
case "double":
case "byte":
case "short":
case "integer":
case "long":
case "scaled_float":
case "half_float":
Serialize<INumberProperty>(ref writer, value, formatterResolver);
break;
case "date":
Serialize<IDateProperty>(ref writer, value, formatterResolver);
break;
case "boolean":
Serialize<IBooleanProperty>(ref writer, value, formatterResolver);
break;
case "binary":
Serialize<IBinaryProperty>(ref writer, value, formatterResolver);
break;
case "object":
Serialize<IObjectProperty>(ref writer, value, formatterResolver);
break;
case "nested":
Serialize<INestedProperty>(ref writer, value, formatterResolver);
break;
case "ip":
Serialize<IIpProperty>(ref writer, value, formatterResolver);
break;
case "geo_point":
Serialize<IGeoPointProperty>(ref writer, value, formatterResolver);
break;
case "geo_shape":
Serialize<IGeoShapeProperty>(ref writer, value, formatterResolver);
break;
case "completion":
Serialize<ICompletionProperty>(ref writer, value, formatterResolver);
break;
case "token_count":
Serialize<ITokenCountProperty>(ref writer, value, formatterResolver);
break;
case "murmur3":
Serialize<IMurmur3HashProperty>(ref writer, value, formatterResolver);
break;
case "percolator":
Serialize<IPercolatorProperty>(ref writer, value, formatterResolver);
break;
case "date_range":
Serialize<IDateRangeProperty>(ref writer, value, formatterResolver);
break;
case "double_range":
Serialize<IDoubleRangeProperty>(ref writer, value, formatterResolver);
break;
case "float_range":
Serialize<IFloatRangeProperty>(ref writer, value, formatterResolver);
break;
case "integer_range":
Serialize<IIntegerRangeProperty>(ref writer, value, formatterResolver);
break;
case "long_range":
Serialize<ILongRangeProperty>(ref writer, value, formatterResolver);
break;
case "ip_range":
Serialize<IIpRangeProperty>(ref writer, value, formatterResolver);
break;
case "join":
Serialize<IJoinProperty>(ref writer, value, formatterResolver);
break;
case "alias":
Serialize<IFieldAliasProperty>(ref writer, value, formatterResolver);
break;
default:
if (value is IGenericProperty genericProperty)
Serialize<IGenericProperty>(ref writer, genericProperty, formatterResolver);
else
{
var formatter = DynamicObjectResolver.ExcludeNullCamelCase.GetFormatter<IProperty>();
formatter.Serialize(ref writer, value, formatterResolver);
}
break;
}

With such strictness, it is not currently possible to derive a type from a NEST type, add additional properties, and have those properties serialized by the internal serializer. This is demonstrated in InjectACustomIPropertyImplementation()

[U (Skip = "TODO: Does not work with utf8json")]
public void InjectACustomIPropertyImplementation()
{
/**
* `PropertyNameAttribute` can be used to mark properties that should be serialized. Without this attribute,
* NEST won't pick up the property for serialization.
*
* Now that we have our own `IProperty` implementation we can add it to our propertes mapping when creating an index
*/
var createIndexResponse = _client.CreateIndex("myindex", c => c
.Map<Project>(m => m
.Properties(props => props
.Custom(new MyPluginProperty("fieldName", "dutch"))
)
)
);
/**
* which will serialize to the following JSON request
*/
// json
var expected = new
{
mappings = new
{
properties = new
{
fieldName = new
{
type = "my_plugin_property",
language = "dutch",
numeric = true
}
}
}
};
/**
* Whilst NEST can _serialize_ our `my_plugin_property`, it does not know how to _deserialize_ it;
* We plan to make this more pluggable in the future
*/
// hide
Expect(expected).FromRequest(createIndexResponse);
}
}

utf8json is able to support this feature through using the fallback formatter, DynamicObjectTypeFallbackFormatter ,exposed by resolving a IJsonFormatter<object>, which internally inspects the type and generates a custom serialization method for it, caching it in a ThreadsafeTypeKeyHashTable<KeyValuePair<object, SerializeMethod>>. This path looks like it would be a plausible way to support extending NEST types but requires some guarding when serializing Attribute types; when a type to serialize is an Attribute type, the walking of properties performed by MetaType traverses down to a framework enum type with multiple members with the same undelying value, which trips up the EnumFormatter. For an Attribute type, this walking should not be performed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions