Динамические свойства case Pascal с Json.NET

Это то, что у меня есть:

using Newtonsoft.Json;

var json = "{\"someProperty\":\"some value\"}";
dynamic deserialized = JsonConvert.DeserializeObject(json);

Это отлично работает:

Assert.That(deserialized.someProperty.ToString(), Is.EqualTo("some value"));

Я хочу, чтобы это работало (первая буква свойств с верхним расположением) без изменения json:

Assert.That(deserialized.someProperty.ToString(), Is.EqualTo("some value"));

Ответ 1

Я согласен с Авнером Шахар-Каштан. Вы не должны делать это, особенно если у вас нет контроля над JSON.

Тем не менее, это можно сделать с помощью ExpandoObject и пользовательского ExpandoObjectConverter. JSON.NET уже предоставляет ExpandoObjectConverter, поэтому с небольшими корректировками у вас есть то, что вы хотите.

Обратите внимание на комментарии //CHANGED внутри фрагмента кода, чтобы показать вам, где я его изменил.

public class CamelCaseToPascalCaseExpandoObjectConverter : JsonConverter
{
  //CHANGED
  //the ExpandoObjectConverter needs this internal method so we have to copy it
  //from JsonReader.cs
  internal static bool IsPrimitiveToken(JsonToken token) 
  {
      switch (token)
      {
          case JsonToken.Integer:
          case JsonToken.Float:
          case JsonToken.String:
          case JsonToken.Boolean:
          case JsonToken.Null:
          case JsonToken.Undefined:
          case JsonToken.Date:
          case JsonToken.Bytes:
              return true;
          default:
              return false;
      }
  }

/// <summary>
/// Writes the JSON representation of the object.
/// </summary>
/// <param name="writer">The <see cref="JsonWriter"/> to write to.</param>
/// <param name="value">The value.</param>
/// <param name="serializer">The calling serializer.</param>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
  // can write is set to false
}

/// <summary>
/// Reads the JSON representation of the object.
/// </summary>
/// <param name="reader">The <see cref="JsonReader"/> to read from.</param>
/// <param name="objectType">Type of the object.</param>
/// <param name="existingValue">The existing value of object being read.</param>
/// <param name="serializer">The calling serializer.</param>
/// <returns>The object value.</returns>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
  return ReadValue(reader);
}

private object ReadValue(JsonReader reader)
{
  while (reader.TokenType == JsonToken.Comment)
  {
    if (!reader.Read())
      throw new Exception("Unexpected end.");
  }

  switch (reader.TokenType)
  {
    case JsonToken.StartObject:
      return ReadObject(reader);
    case JsonToken.StartArray:
      return ReadList(reader);
    default:
      //CHANGED
      //call to static method declared inside this class
      if (IsPrimitiveToken(reader.TokenType))
        return reader.Value;

      //CHANGED
      //Use string.format instead of some util function declared inside JSON.NET
      throw new Exception(string.Format(CultureInfo.InvariantCulture, "Unexpected token when converting ExpandoObject: {0}", reader.TokenType));
  }
}

private object ReadList(JsonReader reader)
{
  IList<object> list = new List<object>();

  while (reader.Read())
  {
    switch (reader.TokenType)
    {
      case JsonToken.Comment:
        break;
      default:
        object v = ReadValue(reader);

        list.Add(v);
        break;
      case JsonToken.EndArray:
        return list;
    }
  }

  throw new Exception("Unexpected end.");
}

private object ReadObject(JsonReader reader)
{
  IDictionary<string, object> expandoObject = new ExpandoObject();

  while (reader.Read())
  {
    switch (reader.TokenType)
    {
      case JsonToken.PropertyName:
        //CHANGED
        //added call to ToPascalCase extension method       
        string propertyName = reader.Value.ToString().ToPascalCase();

        if (!reader.Read())
          throw new Exception("Unexpected end.");

        object v = ReadValue(reader);

        expandoObject[propertyName] = v;
        break;
      case JsonToken.Comment:
        break;
      case JsonToken.EndObject:
        return expandoObject;
    }
  }

  throw new Exception("Unexpected end.");
}

/// <summary>
/// Determines whether this instance can convert the specified object type.
/// </summary>
/// <param name="objectType">Type of the object.</param>
/// <returns>
///     <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
/// </returns>
public override bool CanConvert(Type objectType)
{
  return (objectType == typeof (ExpandoObject));
}

/// <summary>
/// Gets a value indicating whether this <see cref="JsonConverter"/> can write JSON.
/// </summary>
/// <value>
///     <c>true</c> if this <see cref="JsonConverter"/> can write JSON; otherwise, <c>false</c>.
/// </value>
public override bool CanWrite
{
  get { return false; }
}
}

Простая строка для конвертера Case Pascal. Сделайте это умнее, если вам нужно.

public static class StringExtensions
{
    public static string ToPascalCase(this string s)
    {
        if (string.IsNullOrEmpty(s) || !char.IsLower(s[0]))
            return s;

        string str = char.ToUpper(s[0], CultureInfo.InvariantCulture).ToString((IFormatProvider)CultureInfo.InvariantCulture);

        if (s.Length > 1)
            str = str + s.Substring(1);

        return str;
    }
}

Теперь вы можете использовать его так.

var settings = new JsonSerializerSettings()
                   {
                       ContractResolver = new CamelCasePropertyNamesContractResolver(),
                       Converters = new List<JsonConverter> { new CamelCaseToPascalCaseExpandoObjectConverter() }
                   };

var json = "{\"someProperty\":\"some value\"}";

dynamic deserialized = JsonConvert.DeserializeObject<ExpandoObject>(json, settings);

Console.WriteLine(deserialized.SomeProperty); //some value

var json2 = JsonConvert.SerializeObject(deserialized, Formatting.None, settings);

Console.WriteLine(json == json2); //true

CamelCasePropertyNamesContractResolver используется при сериализации объекта обратно в JSON и снова делает его верблюжьем. Это также предоставляет JSON.NET. Если вам это не нужно, вы можете опустить его.

Ответ 2

Я не могу не чувствовать, что это не очень хорошая идея. Похоже, вы пытаетесь сохранить соглашение о кодировании, но за счет сохранения верности между форматом проводов (структурами JSON) и вашими логическими классами. Это может вызвать путаницу для разработчиков, которые ожидают, что классы JSON будут сохранены, и могут вызвать проблемы, если вам также понадобится или понадобится в будущем, чтобы повторно сериализовать эти данные в один и тот же формат JSON.

Тем не менее, это, вероятно, может быть достигнуто путем создания классов .NET раньше времени, а затем с помощью DeserializeObject(string value, JsonSerializerSettings settings) перегрузки, передачи его a JsonSerializerSettings с набором свойств Binder. Вам также нужно будет написать пользовательский SerializationBinder, в котором вы вручную определяете связь между вашим классом JSON и вашим предопределенным классом .NET.

Возможно, можно генерировать классы с паскалью в среде выполнения без предопределения их, но я недостаточно разбирался в реализации JSON.NET для этого. Возможно, это одна из других настроек JsonSerializerSettings, например, передача CustomCreationConverter, но я не уверен в деталях.

Ответ 3

вам нужно изменить свой JSON, {\"SomeProperty\":\"some value\"}