JSON.net: jak deserializować bez użycia domyślnego konstruktora?

141

Mam klasę, która ma domyślny konstruktor, a także przeciążony konstruktor, który przyjmuje zestaw parametrów. Parametry te pasują do pól na obiekcie i są przypisywane podczas budowy. W tym momencie potrzebuję domyślnego konstruktora do innych celów, więc chciałbym go zachować, jeśli mogę.

Mój problem: Jeśli usunę domyślny konstruktor i przekażę ciąg JSON, obiekt deserializuje się poprawnie i przekazuje parametry konstruktora bez żadnych problemów. Skończyło się na tym, że obiekt został zapełniony tak, jak bym się spodziewał. Jednak gdy tylko dodam domyślny konstruktor do obiektu, po wywołaniu JsonConvert.DeserializeObject<Result>(jsontext)właściwości nie są już zapełniane.

W tym momencie próbowałem dodać new JsonSerializerSettings(){CheckAdditionalContent = true}do wywołania deserializacji. to nic nie dało.

Kolejna uwaga. parametry contructor dokładnie odpowiadają nazwom pól, z wyjątkiem tego, że parametry zaczynają się od małej litery. Nie sądziłbym, że to ma znaczenie, ponieważ, jak wspomniałem, deserializacja działa dobrze bez domyślnego konstruktora.

Oto próbka moich konstruktorów:

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
źródło

Odpowiedzi:

216

Json.Net preferuje użycie domyślnego (bezparametrowego) konstruktora na obiekcie, jeśli taki istnieje. Jeśli istnieje wiele konstruktorów i chcesz, aby Json.Net używał innego niż domyślny, możesz dodać [JsonConstructor]atrybut do konstruktora, który ma wywoływać Json.Net.

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

Ważne jest, aby nazwy parametrów konstruktora były zgodne z odpowiednimi nazwami właściwości obiektu JSON (ignorując wielkość liter), aby to działało poprawnie. Nie musisz jednak koniecznie mieć parametru konstruktora dla każdej właściwości obiektu. W przypadku właściwości obiektów JSON, które nie są objęte parametrami konstruktora, Json.Net spróbuje użyć metod dostępu do właściwości publicznych (lub właściwości / pól oznaczonych[JsonProperty] ), aby wypełnić obiekt po jego skonstruowaniu.

Jeśli nie chcesz dodawać atrybutów do swojej klasy lub w inny sposób nie kontrolujesz kodu źródłowego klasy, którą próbujesz deserializować, inną alternatywą jest utworzenie niestandardowego JsonConverter w celu utworzenia wystąpienia i wypełnienia obiektu. Na przykład:

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();
    }
}

Następnie dodaj konwerter do ustawień serializatora i użyj ustawień podczas deserializacji:

JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Converters.Add(new ResultConverter());
Result result = JsonConvert.DeserializeObject<Result>(jsontext, settings);
Brian Rogers
źródło
5
To zadziałało. To trochę do bani, że teraz muszę wziąć zależność JSON.net w moim projekcie modeli, ale co tam. Oznaczę to jako odpowiedź.
kmacdonald
3
Istnieją inne opcje - możesz utworzyć niestandardowy JsonConverterdla swojej klasy. Spowodowałoby to usunięcie zależności, ale wtedy musiałbyś samodzielnie zająć się tworzeniem wystąpienia i zapełnianiem obiektu w konwerterze. Możliwe jest również napisanie niestandardowego ContractResolver, który nakierowałby Json.Net na użycie innego konstruktora, zmieniając jego JsonObjectContract, ale może się to okazać nieco trudniejsze, niż się wydaje.
Brian Rogers,
Tak, myślę, że atrybut będzie działał dobrze. Wywołanie deserializacji jest w rzeczywistości ogólne, więc może być obiektem dowolnego typu. myślę, że Twoja pierwotna odpowiedź będzie działać dobrze. dzięki za informację!
kmacdonald
3
Naprawdę pomogłoby, gdyby można było ustawić inną konwencję wyboru konstruktora. Na przykład myślę, że kontener Unity obsługuje to. Wtedy można by było tak zrobić, aby zawsze wybierał konstruktor z większością parametrów, zamiast wracać do domyślnego. Czy istnieje możliwość takiego punktu rozszerzenia w Json.Net?
julealgon
1
Nie zapomnijusing Newtonsoft.Json;
Bruno Bieri
40

Trochę późno i nie do końca pasuję tutaj, ale dodam tutaj moje rozwiązanie, ponieważ moje pytanie zostało zamknięte jako duplikat tego, a to rozwiązanie jest zupełnie inne.

Potrzebowałem ogólnego sposobu poinstruowania, Json.NETaby preferować najbardziej szczegółowy konstruktor dla typu struktury zdefiniowanego przez użytkownika, więc mogę pominąć JsonConstructoratrybuty, które dodałyby zależność do projektu, w którym każda taka struktura jest zdefiniowana.

Wykonałem trochę inżynierii wstecznej i zaimplementowałem niestandardowy program do rozpoznawania kontraktów, w którym nadpisałem CreateObjectContractmetodę dodawania niestandardowej logiki tworzenia.

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);
    }
}

Używam tego w ten sposób.

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");
Zoltán Tamási
źródło
2
Obecnie korzystam z zaakceptowanej odpowiedzi powyżej, ale chcę również podziękować za pokazanie rozwiązania!
DotBert
1
Usunąłem ograniczenie dotyczące struktur (czek na objectType.IsValueType) i działa świetnie, dzięki!
Alex Angas
@AlexAngas Tak, ogólnie rzecz biorąc, stosowanie tej strategii ma sens, dziękuję za Twoją opinię.
Zoltán Tamási
3

Na podstawie niektórych odpowiedzi tutaj napisałem plik CustomConstructorResolver do wykorzystania w bieżącym projekcie i pomyślałem, że może to pomóc komuś innemu.

Obsługuje następujące mechanizmy rozdzielczości, wszystkie konfigurowalne:

  • Wybierz jeden prywatny konstruktor, aby móc zdefiniować jeden prywatny konstruktor bez konieczności oznaczania go atrybutem.
  • Wybierz najbardziej szczegółowy konstruktor prywatny, aby móc mieć wiele przeciążeń, nadal bez konieczności używania atrybutów.
  • Wybierz konstruktor oznaczony atrybutem o określonej nazwie - jak domyślny program rozpoznawania nazw, ale bez zależności od pakietu Json.Net, ponieważ musisz się odwołać 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;
    }
}

Oto pełna wersja z dokumentacją XML jako sednem: https://gist.github.com/maverickelementalch/80f77f4b6bdce3b434b0f7a1d06baa95

Opinie mile widziane.

Björn Jarisch
źródło
Świetne rozwiązanie! Dzięki za udostępnienie.
thomai
1

Domyślne zachowanie Newtonsoft.Json będzie znajdować plik public konstruktorów. Jeśli Twój domyślny konstruktor jest używany tylko w zawierającym klasę lub ten sam zestaw, możesz zmniejszyć poziom dostępu do protectedlub internaltak, aby Newtonsoft.Json wybierał żądany publickonstruktor.

Trzeba przyznać, że to rozwiązanie jest raczej bardzo ograniczone do konkretnych przypadków.

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
źródło
-1

Rozwiązanie:

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;
}

Model:

public class modelname {
    public long parameter{ get; set; }
    public int parameter{ get; set; }
    public int parameter{ get; set; }
    public string parameter{ get; set; }
}
sachin
źródło