Indexing JToken objects ignores DefaultFieldNameInferrer
See original GitHub issueNEST/Elasticsearch.Net version: 2.4.4
Elasticsearch version: 2.3.3
Description of the problem including expected versus actual behavior:
When we Index any object, we normally run through the DefaultFieldNameInferrer
- using that we can control the property names in ES. We’ve used this to fix all names to be lowercase in our project.
But when we index anything containing a JToken
, the DefaultFieldNameInferrer
is ignored.
Steps to reproduce:
Use the following code.
class Program
{
static void Main(string[] args)
{
ConnectionSettings settings = new ConnectionSettings(new SingleNodeConnectionPool(new Uri("http://127.0.0.1:9200/")));
settings.DefaultFieldNameInferrer(s => s.ToLower());
settings.DefaultIndex("testindex");
settings.ThrowExceptions();
ElasticClient client = new ElasticClient(settings);
var item = new MyClass { MyInteger = 5, MyString = "Hello world" };
var itemAsJtoken = JToken.FromObject(item);
client.Index(itemAsJtoken);
}
}
public class MyClass
{
public string MyString { get; set; }
public int MyInteger { get; set; }
}
Observe that the auto-created mapping on testindex
is as follows:
{
"testindex" : {
"mappings" : {
"jtoken" : {
"properties" : {
"MyInteger" : {
"type" : "long"
},
"MyString" : {
"type" : "string"
}
}
}
}
}
}
If we index the item
instead of itemAsJtoken
, we get the following map:
{
"testindex" : {
"mappings" : {
"myclass" : {
"properties" : {
"myinteger" : {
"type" : "long"
},
"mystring" : {
"type" : "string"
}
}
}
}
}
}
Relevant sources:
We found an issue in Newtonsoft.NET, where they’ve changed the way the contract resolvers are called. It seems that the DefaultContractResolver
in Newtonsoft no longer calls its ResolvePropertyName
method, which is used internally by NEST. The issue for that is here:
JamesNK/Newtonsoft.Json/issues/950
We can reproduce this by creating the following code:
class Program
{
static void Main(string[] args)
{
var item = new MyClass { MyInteger = 5, MyString = "Hello world" };
var itemAsJtoken = JToken.FromObject(item);
var serializerSettings = new JsonSerializerSettings();
serializerSettings.ContractResolver = new MyContractResolver();
var serializer = JsonSerializer.Create(serializerSettings);
StringBuilder sb = new StringBuilder();
using (var sw = new StringWriter(sb))
serializer.Serialize(sw, itemAsJtoken);
Console.WriteLine(sb.ToString());
}
}
public class MyClass
{
public string MyString { get; set; }
public int MyInteger { get; set; }
}
internal class MyContractResolver : DefaultContractResolver
{
protected override string ResolvePropertyName(string propertyName)
{
return propertyName.ToLower();
}
}
Relevant people (on my end): @genbox
Issue Analytics
- State:
- Created 7 years ago
- Comments:5 (5 by maintainers)
Top GitHub Comments
I’ve looked further into this.
JToken.FromObject(object o)
will use the default Json.NET conventions for how property names are serialized at serialization time; theNamingStrategy
is used at conversion time fromobject
toJToken
, using theResolvePropertyName()
method of the Contract resolver (delegating to theNamingStrategy
) on the serializer passed toJToken.FromObject(object o, JsonSerializer serializer)
. In the case of the method that does not take aJsonSerializer
instance, an instance will be constructed usingJsonSerializer.CreateDefault()
and passed to the overloaded version that does.With the above in mind, in order for serialization of a
JObject
to adhere to the default field inference, an instance of aJsonSerializer
needs to be passed for the conversion that uses the same rules as the contract resolver used by NEST. TheElasticContractResolver
overridesResolvePropertyName(string name)
and doesn’t call the base method, so we don’t need a customNamingStrategy
, NEST will work as is with theDefaultFieldNameInferrer
, so long as theJsonSerializer
used for conversion usesElasticConstractResolver
.Here is where things are a little tricky though; because NEST defines an interface for a serializer,
IElasticsearchSerializer
, it doesn’t directly expose the Json.NET serializer that is used under the covers in the default case (which may not be Json.NET at all for someone using a custom serializer). The contract resolver is exposed however, but as aprotected
property, so we can get at it with a derived type. We can use the contract resolver in the construction of aJsonSerializer
to pass toJToken.FromObject(object o, JsonSerializer serializer)
.Here’s an example that will work
Ok I’m good with closing with a known workaround of calling
JObject.FromObject(obj, serializer)
@LordMike closing this but please reply if you feel strongly that this a client concern that we should handle!