JSON.net: как десериализовать без использования конструктора по умолчанию?

136

У меня есть класс, в котором есть конструктор по умолчанию, а также перегруженный конструктор, который принимает набор параметров. Эти параметры соответствуют полям объекта и назначаются при построении. На данный момент мне нужен конструктор по умолчанию для других целей, поэтому я хотел бы сохранить его, если смогу.

Моя проблема: если я удалю конструктор по умолчанию и передаю строку JSON, объект десериализуется правильно и без проблем передает параметры конструктора. В итоге я возвращаю объект, заполненный так, как я ожидал. Однако, как только я добавляю в объект конструктор по умолчанию, когда я вызываю, JsonConvert.DeserializeObject<Result>(jsontext)свойства больше не заполняются.

На этом этапе я попытался добавить new JsonSerializerSettings(){CheckAdditionalContent = true}к вызову десериализации. это ничего не дало.

Еще одно замечание. параметры конструктора точно совпадают с названиями полей, за исключением того, что параметры начинаются со строчной буквы. Я бы не подумал, что это будет иметь значение, поскольку, как я уже упоминал, десериализация отлично работает без конструктора по умолчанию.

Вот образец моих конструкторов:

public Result() { }

public Result(int? code, string format, Dictionary<string, string> details = null)
{
    Code = code ?? ERROR_CODE;
    Format = format;

    if (details == null)
        Details = new Dictionary<string, string>();
    else
        Details = details;
}
kmacdonald
источник

Ответы:

209

Json.Net предпочитает использовать конструктор по умолчанию (без параметров) для объекта, если он есть. Если имеется несколько конструкторов, и вы хотите, чтобы Json.Net использовал конструктор, отличный от конструктора по умолчанию, вы можете добавить [JsonConstructor]атрибут в конструктор, который должен вызывать Json.Net.

[JsonConstructor]
public Result(int? code, string format, Dictionary<string, string> details = null)
{
    ...
}

Для правильной работы важно, чтобы имена параметров конструктора совпадали с соответствующими именами свойств объекта JSON (без учета регистра). Однако необязательно иметь параметр конструктора для каждого свойства объекта. Для тех свойств объекта JSON, которые не охвачены параметрами конструктора, Json.Net попытается использовать средства доступа к общедоступным свойствам (или свойства / поля, отмеченные значком [JsonProperty]) для заполнения объекта после его создания.

Если вы не хотите добавлять атрибуты в свой класс или иным образом не управляете исходным кодом для класса, который вы пытаетесь десериализовать, то другой альтернативой является создание настраиваемого JsonConverter для создания экземпляра и заполнения вашего объекта. Например:

class ResultConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(Result));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // Load the JSON for the Result into a JObject
        JObject jo = JObject.Load(reader);

        // Read the properties which will be used as constructor parameters
        int? code = (int?)jo["Code"];
        string format = (string)jo["Format"];

        // Construct the Result object using the non-default constructor
        Result result = new Result(code, format);

        // (If anything else needs to be populated on the result object, do that here)

        // Return the result
        return result;
    }

    public override bool CanWrite
    {
        get { return false; }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Затем добавьте конвертер в настройки сериализатора и используйте настройки при десериализации:

JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Converters.Add(new ResultConverter());
Result result = JsonConvert.DeserializeObject<Result>(jsontext, settings);
Брайан Роджерс
источник
4
Это сработало. Отстойно, что теперь мне приходится использовать зависимость JSON.net в моем проекте моделей, но какого черта. Отмечу это как ответ.
kmacdonald
3
Есть и другие варианты - вы можете создать индивидуальный заказ JsonConverterдля своего класса. Это удалит зависимость, но тогда вам придется самостоятельно обрабатывать создание и заполнение объекта в конвертере. Также можно было бы написать обычай, ContractResolverкоторый заставил бы Json.Net использовать другой конструктор, изменив его JsonObjectContract, но это может оказаться немного сложнее, чем кажется.
Брайан Роджерс
Да, я думаю, атрибут будет работать нормально. Вызов десериализации на самом деле является универсальным, так что это может быть любой тип объекта. Я думаю, ваш исходный ответ подойдет. Спасибо за информацию!
kmacdonald
2
Было бы действительно полезно, если бы можно было установить другое соглашение для выбора конструктора. Например, я думаю, что контейнер Unity поддерживает это. Затем вы можете сделать так, чтобы он всегда выбирал конструктор с большинством параметров, а не возвращался к конструктору по умолчанию. Есть ли возможность такой точки расширения в Json.Net?
julealgon
1
Не забывайтеusing Newtonsoft.Json;
Бруно
36

Немного поздно и не совсем подходит здесь, но я собираюсь добавить свое решение здесь, потому что мой вопрос был закрыт как дубликат этого, и потому что это решение совершенно другое.

Мне нужен был общий способ указать Json.NETпредпочтение наиболее конкретному конструктору для определенного пользователем типа структуры, поэтому я могу опустить JsonConstructorатрибуты, которые добавили бы зависимость к проекту, в котором определена каждая такая структура.

Я немного реконструировал и реализовал настраиваемый преобразователь контрактов, в котором я переопределил CreateObjectContractметод, чтобы добавить свою собственную логику создания.

public class CustomContractResolver : DefaultContractResolver {

    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        var c = base.CreateObjectContract(objectType);
        if (!IsCustomStruct(objectType)) return c;

        IList<ConstructorInfo> list = objectType.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).OrderBy(e => e.GetParameters().Length).ToList();
        var mostSpecific = list.LastOrDefault();
        if (mostSpecific != null)
        {
            c.OverrideCreator = CreateParameterizedConstructor(mostSpecific);
            c.CreatorParameters.AddRange(CreateConstructorParameters(mostSpecific, c.Properties));
        }

        return c;
    }

    protected virtual bool IsCustomStruct(Type objectType)
    {
        return objectType.IsValueType && !objectType.IsPrimitive && !objectType.IsEnum && !objectType.Namespace.IsNullOrEmpty() && !objectType.Namespace.StartsWith("System.");
    }

    private ObjectConstructor<object> CreateParameterizedConstructor(MethodBase method)
    {
        method.ThrowIfNull("method");
        var c = method as ConstructorInfo;
        if (c != null)
            return a => c.Invoke(a);
        return a => method.Invoke(null, a);
    }
}

Пользуюсь вот так.

public struct Test {
  public readonly int A;
  public readonly string B;

  public Test(int a, string b) {
    A = a;
    B = b;
  }
}

var json = JsonConvert.SerializeObject(new Test(1, "Test"), new JsonSerializerSettings {
  ContractResolver = new CustomContractResolver()
});
var t = JsonConvert.DeserializeObject<Test>(json);
t.A.ShouldEqual(1);
t.B.ShouldEqual("Test");
Золтан Тамаши
источник
2
В настоящее время я использую принятый выше ответ, но хочу поблагодарить вас за то, что вы показали свое решение!
DotBert
1
Я снял ограничение на структуры (проверка objectType.IsValueType), и это отлично работает, спасибо!
Alex Angas
@AlexAngas Да, применение этой стратегии в целом имеет смысл, спасибо за ваш отзыв.
Золтан Тамаши
3

Основываясь на некоторых ответах здесь, я написал CustomConstructorResolverдля использования в текущем проекте и подумал, что это может помочь кому-то другому.

Он поддерживает следующие механизмы разрешения, все настраиваемые:

  • Выберите один частный конструктор, чтобы вы могли определить один частный конструктор, не отмечая его атрибутом.
  • Выберите наиболее конкретный частный конструктор, чтобы у вас было несколько перегрузок, но без использования атрибутов.
  • Выберите конструктор, помеченный атрибутом с определенным именем - например, преобразователь по умолчанию, но без зависимости от пакета Json.Net, потому что вам нужно ссылаться Newtonsoft.Json.JsonConstructorAttribute.
public class CustomConstructorResolver : DefaultContractResolver
{
    public string ConstructorAttributeName { get; set; } = "JsonConstructorAttribute";
    public bool IgnoreAttributeConstructor { get; set; } = false;
    public bool IgnoreSinglePrivateConstructor { get; set; } = false;
    public bool IgnoreMostSpecificConstructor { get; set; } = false;

    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        var contract = base.CreateObjectContract(objectType);

        // Use default contract for non-object types.
        if (objectType.IsPrimitive || objectType.IsEnum) return contract;

        // Look for constructor with attribute first, then single private, then most specific.
        var overrideConstructor = 
               (this.IgnoreAttributeConstructor ? null : GetAttributeConstructor(objectType)) 
            ?? (this.IgnoreSinglePrivateConstructor ? null : GetSinglePrivateConstructor(objectType)) 
            ?? (this.IgnoreMostSpecificConstructor ? null : GetMostSpecificConstructor(objectType));

        // Set override constructor if found, otherwise use default contract.
        if (overrideConstructor != null)
        {
            SetOverrideCreator(contract, overrideConstructor);
        }

        return contract;
    }

    private void SetOverrideCreator(JsonObjectContract contract, ConstructorInfo attributeConstructor)
    {
        contract.OverrideCreator = CreateParameterizedConstructor(attributeConstructor);
        contract.CreatorParameters.Clear();
        foreach (var constructorParameter in base.CreateConstructorParameters(attributeConstructor, contract.Properties))
        {
            contract.CreatorParameters.Add(constructorParameter);
        }
    }

    private ObjectConstructor<object> CreateParameterizedConstructor(MethodBase method)
    {
        var c = method as ConstructorInfo;
        if (c != null)
            return a => c.Invoke(a);
        return a => method.Invoke(null, a);
    }

    protected virtual ConstructorInfo GetAttributeConstructor(Type objectType)
    {
        var constructors = objectType
            .GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
            .Where(c => c.GetCustomAttributes().Any(a => a.GetType().Name == this.ConstructorAttributeName)).ToList();

        if (constructors.Count == 1) return constructors[0];
        if (constructors.Count > 1)
            throw new JsonException($"Multiple constructors with a {this.ConstructorAttributeName}.");

        return null;
    }

    protected virtual ConstructorInfo GetSinglePrivateConstructor(Type objectType)
    {
        var constructors = objectType
            .GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);

        return constructors.Length == 1 ? constructors[0] : null;
    }

    protected virtual ConstructorInfo GetMostSpecificConstructor(Type objectType)
    {
        var constructors = objectType
            .GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
            .OrderBy(e => e.GetParameters().Length);

        var mostSpecific = constructors.LastOrDefault();
        return mostSpecific;
    }
}

Вот полная версия с XML-документацией как суть: https://gist.github.com/maverickelementalch/80f77f4b6bdce3b434b0f7a1d06baa95

Обратная связь приветствуется.

Бьорн Яриш
источник
Отличное решение! Спасибо, что поделился.
thomai
1

По умолчанию Newtonsoft.Json будет искать publicконструкторы. Если ваш конструктор по умолчанию используется только в содержащем классе или той же сборке, вы можете уменьшить уровень доступа до protectedили internalтак, чтобы Newtonsoft.Json выбрал нужный publicконструктор.

По общему признанию, это решение весьма ограничено конкретными случаями.

internal Result() { }

public Result(int? code, string format, Dictionary<string, string> details = null)
{
    Code = code ?? ERROR_CODE;
    Format = format;

    if (details == null)
        Details = new Dictionary<string, string>();
    else
        Details = details;
}
Darkato
источник
-1

Решение:

public Response Get(string jsonData) {
    var json = JsonConvert.DeserializeObject<modelname>(jsonData);
    var data = StoredProcedure.procedureName(json.Parameter, json.Parameter, json.Parameter, json.Parameter);
    return data;
}

Модель:

public class modelname {
    public long parameter{ get; set; }
    public int parameter{ get; set; }
    public int parameter{ get; set; }
    public string parameter{ get; set; }
}
Сэчин
источник