Giter VIP home page Giter VIP logo

dynamicjson's Introduction

DynamicJsonLogo

DynamicJson is a UnityEngine specific UPM package that enhances the capabilities of newtonsoft.json by providing a straightforward, intuitive, and generalized deserialization of complex polymorphic models. As well as other tools to simplify workflows with JSONs.

Installation

To add this package to your Unity project:

  • Open the Package Manager.
  • Press the Add button.
  • Select Add package from git URL.
  • Enter the URL: https://github.com/DysonCore/DynamicJson.git.

For a detailed guide, refer to Unity How to Install a package from a Git URL.

Dependencies

This package requires the Newtonsoft.Json library to be installed.
You can add it as a UPM package.

Converters

Polymorphic converter Adds the ability to deserialize JSON with polymorphic content straight into c# instances without creating custom converters for each class.

Description

  • PolymorphicConverter - provides custom JSON deserialization for objects annotated with TypifyingProperty attribute.
  • TypifyingPropertyAttribute - designates a property for polymorphic deserialization as a qualifier. This attribute should be applied both on the property declaration and its value assignment.
  • TypifiedPropertyAttribute - allows to deserialize class members with the same TypifyingPropertyAttribute value as the main class.

Usage

Start by marking the qualifying property (either abstract or virtual) in the base class with [TypifyingProperty] attribute.

public abstract class Reward
{
    // Can be used in conjunction with the [JsonProperty] attribute. 
    [TypifyingProperty]
    public abstract string RewardType { get; }
}

In the derived classes, annotate the same property again and assign a unique value that identifies each class.

public class CurrencyReward : Reward
{
    [TypifyingProperty]
    // Tip: use the 'sealed' keyword if You don't intend 
    // to further inherit from this class and override the qualifying value.  
    public sealed override string RewardType => "currency";
    
    public int Amount { get; set; }
}
public class BadgeReward : Reward
{
    [TypifyingProperty]
    public sealed override string RewardType => "badge";
    
    public string BadgeId { get; set; }
}

Lastly, integrate the PolymorphicConverter into JsonSerializer or JsonSerializerSettings (preferred method).

var settings = new JsonSerializerSettings();
settings.Converters.Add(new PolymorphicConverter());
var serializer = new JsonSerializer();
serializer.Converters.Add(new PolymorphicConverter());

As another option, You can annotate the base class with [JsonConverter(typeof(PolymorphicConverter))] (though it is not a recommended approach).

[JsonConverter(typeof(PolymorphicConverter))]
public abstract class Reward
{
    //fields and properties...
}

With this setup, You can execute the following:

List<Reward> rewards = new List<Reward>
{
    new CurrencyReward{Amount = 100},
    new BadgeReward{BadgeId = "newbie_badge_01"}
};
            
string rewardsJson = JsonConvert.SerializeObject(rewards);
// rewardsJson value: [{"RewardType":"currency","Amount":100},{"RewardType":"badge","BadgeId":"newbie_badge_01"}]
List<Reward> deserializedRewards = JsonConvert.DeserializeObject<List<Reward>>(rewardsJson); 
// Note: The above has been tested with the [JsonConverter(typeof(PolymorphicConverter))] attribute applied to the Reward class.
}

Despite using the abstract Reward class for deserialization, the deserializedRewards list will correctly contain instances of the concrete CurrencyReward and BadgeReward classes.

Complex Inheritance

PolymorphicConverter is designed to address the challenges posed by intricate inheritance hierarchies in polymorphism.

Consider the scenario of an abstract Animal class. This base class has an abstract inheritor Mammal, which in turn can be inherited by other concrete classes like Dog or Cat.

public abstract class Animal
{
    [TypifyingProperty]
    public abstract string AnimalType { get; }
}
public abstract class Mammal : Animal
{
    [TypifyingProperty]
    public override abstract string AnimalType => "Mammal";

    [TypifyingProperty]
    public abstract string MammalType { get; }
}
public class Dog : Mammal
{
    [TypifyingProperty]
    public sealed override string MammalType => "Dog";

    public string Breed { get; set; }
}
public class Cat : Mammal
{
    [TypifyingProperty]
    public sealed override string MammalType => "Cat";

    public string Color { get; set; }
}

In this example:

  • Animal is the top-level base class with a qualifying attribute AnimalType.
  • Mammal, an abstract derived class, assigns the value "Mammal" to the AnimalType and introduces its own qualifier MammalType.
  • Concrete classes Dog and Cat further override MammalType qualifier to provide specific values.

When deserializing a list of Animal, PolymorphicConverter will inspect the qualifiers and correctly instantiate Dog and Cat objects based on the provided JSON, even with such nested hierarchies.

Typifying types

PolymorphicConverter allows a great deal of flexibility when choosing the type for typifying property. Both value types and reference types which properly implement equality comparison are valid. I.e. override bool Equals(object obj) and override int GetHashCode().

*Tip*: the most concise and convenient type for qualifying property is enum in combination with Newtonsoft.StringEnumConverter or DynamicJson.SafeStringEnumConverter.

Interface as Inheritance Root

PolymorphicConverter can't automatically find references between interface and derived classes. So if You are using interface as an inheritance root, You need to explicitly specify the interface type like so:

public interface IAnimal
{
    [TypifyingProperty]
    AnimalType AnimalType { get; }
}
public class Mammal : IAnimal
{
    [TypifyingProperty(typeof(IAnimal))]
    public AnimalType AnimalType => AnimalType.Mammal;
}

Unknown Value Handling

When PolymorphicConverter encounters unknown value under the TypifyingPropertyAttribute - it has 2 ways to handle it:

  • UnknownTypeHandling.ThrowError - Throws JsonSerializationException.
  • UnknownTypeHandling.ReturnNull - Returns null for an object.

By default UnknownTypeHandling.ThrowError is used. To specify otherwise, pass UnknownTypeHandling Enum as a parameter in PolymorphicConverter constructor.

var settings = new JsonSerializerSettings();
settings.Converters.Add(new PolymorphicConverter(UnknownTypeHandling.ReturnNull));

Typified Properties

The TypifiedPropertyAttribute allows to deserialize polymorphic class members with the same TypifyingPropertyAttribute value as the main class.

Start by declaring the main class with [TypifyingProperty] and another root class of IQuestProgress as [TypifiedProperty].

public class Quest
{
    [TypifyingProperty]
    public QuestType QuestType { get; private set; }

    [TypifiedProperty]
    public IQuestProgress Progress { get; private set; }
}
public enum QuestType
{
    Normal,
    Special
}

IQuestProgress class and its inheritors should have the same structure as a regular polymorphic hierarchy with TypifyingPropertyAttribute.

public interface IQuestProgress
{
    [TypifyingProperty]
    QuestType QuestType { get; }
}
private class NormalQuestProgress : IQuestProgress
{
    [TypifyingProperty(typeof(IQuestProgress))]
    public QuestType QuestType => QuestType.Normal;
}
private class SpecialQuestProgress : IQuestProgress
{
    [TypifyingProperty(typeof(IQuestProgress))]
    public QuestType QuestType => QuestType.Special;
}

With this setup PolymorphicConverter will correctly deserialize IQuestProgress composite member of the Quest class by using [TypifyingProperty] QuestType value from the main class.

Cache initialization

PolymorphicConverter requires knowledge of potential derived types for accurate deserialization. Converter automatically scans assemblies which are referencing the DynamicJson assembly on script recompilation and pre-build process and creates a cache file under Assets/Resources/DynamicJson folder. Do not edit, move or delete the cache file!

Known limitations

  • Plain [TypifyingProperty] can not be used with the interface as an inheritance root! [TypifyingProperty(typeof(Interface))] should be used in derived classes instead.

Remarks

  • Newtonsoft.Json does not support multiple converters on a single class. If you are using PolymorphicConverter's attributes in your base class - make sure it will not be deserialized by another converter.
  • Although unit tests are covering the most common use cases, it is never a bad idea to test your polymorphic models and parsing correctness after initial implementation.

SafeStringEnum converter Adds the fallback for Enum deserialization. If the given json string has no corresponding Enum value - the default one will be used.

Description

SafeStringEnumConverter is an inheritor of Newtonsoft StringEnumConverter and it is designed to safely handle Enum deserialization, providing additional support of fallbacks.

Usage

Use the DefaultEnumValueAttribute to mark an enum member as the default value:

public enum FoodType
{
    [DefaultEnumValue]
    Unknown,
    Pizza,
    Burger
    // other values...
}
public class Plate
{
[JsonConverter(typeof(SafeStringEnumConverter))] //or you can add this converter to JsonSerializerSettings.
public FoodType Food { get; set; }
}

string json = "{\"Food\":\"Sushi\"}"; // sushi is not present in FoodType enum.
Plate plate = JsonConvert.DeserializeObject<Plate>(json);
// Plate.Food will be set to FoodType.Unknown.

In this example, an invalid enum value in the JSON string is safely converted to FoodType.Unknown.


Injection converter (experimental) Adds the ability to inject data from a cache into the deserialized instance based on given identifiers.

Description

InjectionConverter gives an ability to inject data from InjectionDataProviders straight into IInjectable<> members of deserialized instance.

This converter is not nearly as useful as other ones since any IInjectable<TValue> can be replaced by its plain identifier and the data can be retrieved from the cache after the deserialization is finished. But this "data retrieving" operations can occur quite frequently, so this converter can be a good "quality of life" improvement.

  • IInjectable<TValue> - root interface for wrappers over any data model - TValue.
  • InjectionDataProvider<TIdentifier, TValue> - root provider / cache class. Enforces implementation of methods for retrieving data - TValue by the identifier - TIdentifier. Its inheritors can be safely instantiated by any Dependency Injector. Only one InjectionDataProvider can be present at once for any TValue type.
  • EagerInjectable<TValue> - concrete inheritor of IInjectable<TValue>. Will retrieve the data from its corresponding InjectionDataProvider as soon as its identifier is set.
  • LazyInjectable<TValue> - concrete inheritor of IInjectable<TValue>. Will retrieve the data from its corresponding InjectionDataProvider only when its TValue value will be requested.
  • InjectionConverter - replaces any IInjectable<> with its identifier on serialization and puts the identifier into the IInjectable<> on deserialization.

Usage

Lets create 2 models: Weapon and WeaponConfig. WeaponConfigs are predetermined, while Weapons are composite from WeaponConfig and Name property.

public class WeaponConfig
{
    public string Id { get; private set; }
            
    public int Damage { get; private set; }

    public WeaponConfig(string id, int damage)
    {
        Id = id;
        Damage = damage;
    }
}
public class Weapon
{
    [JsonProperty("uid")]
    public EagerInjectable<WeaponConfig> Config { get; private set; }

    [JsonProperty("name")]
    public string Name { get; private set; }

    public Weapon(WeaponConfig config, string name)
    {
        Config = new EagerInjectable<WeaponConfig>(config);
        Name = name;
    }

    [JsonConstructor]
    private Weapon() { }
}

The WeaponConfig is wrapped with EagerInjectable<>, meaning we can deserialize Weapon without full representation of WeaponConfig. Now lets make InjectionDataProvider.

//identifier for WeaponConfig model is a string. But it can be any type. 
public class WeaponInjectionDataProvider : InjectionDataProvider<string, WeaponConfig>
{
    // Data cache.
    private readonly Dictionary<string, WeaponConfig> _data = new ();
    // Used by IInjectable<WeeaponConfig> models to resolve data from identifier.        
    public override WeaponConfig GetValue(string identifier)
    {
        _data.TryGetValue(identifier, out WeaponConfig config);
        return config;
    }
    // Used by InjectionConverter to get identifier for serialization.
    public override string GetIdentifier(WeaponConfig value)
    {
        return value.Id; // Specifies how the identifier is retrieved from the WeaponConfig model. 
    }
    // Is used to fill the cache with data for demonstration purposes.
    public void AddConfig(WeaponConfig config)
    {   
        _data[config.Id] = config;
    }
}

And that is it. Really.
The only thing left is to plug InjectionConverter and test the results. This converter can work both directions (serialization and deserialization).

// Create JSON Settings with InjectionConverter.
var settings = new JsonSerializerSettings();
settings.Converters.Add(new InjectionConverter());
// Instantiate data provider. It will be automatically added to the static registry of providers. 
// Also It can be instantiated by Dependency Injector such as Zenject, StrangeIoC, or any other. 
var weaponDataProvider = new WeaponInjectionDataProvider();
// Create weapon configs. 
WeaponConfig heavyBladeConfig = new WeaponConfig("heavy_blade_01", 100);
WeaponConfig daggerConfig = new WeaponConfig("dagger_01", 20);
// Populate providers cache with data (WeaponConfigs).            
weaponDataProvider.AddConfig(heavyBladeConfig);
weaponDataProvider.AddConfig(daggerConfig);
// Weapons are created manually to test serialization first.            
Weapon heavyWeapon = new Weapon(heavyBladeConfig, "Big spoon");
Weapon daggerWeapon = new Weapon(daggerConfig, "THE pencil");

List<Weapon> weapons = new()
{
    heavyWeapon,
    daggerWeapon,
};

string weaponsString = JsonConvert.SerializeObject(weapons, settings);
// serialized weaponsString value:
// [{"uid":"heavy_blade_01","name":"Big spoon"},{"uid":"dagger_01","name":"THE pencil"}]
// As you can see - it has no data about WeaponConfigs except its identifiers, i.e. uid.  

// Now we can deserialize it back and InjectionParser will get the WeaponConfig data from 
// WeaponDataProvider and set it into the Weapon instances. 
List<Weapon> deserializedWeapons = JsonConvert.DeserializeObject<List<Weapon>>(weaponsString, settings);

Feedback and Contributions

Your feedback is invaluable to DynamicJson improvements. For bug reports, suggestions, feature requests, or contributions, please visit the GitHub repository.

dynamicjson's People

Contributors

github-actions[bot] avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

dynamicjson's Issues

JsonSerializationException: Could not create an instance of type Reward. Type is an interface or abstract class and cannot be instantiated. Path '[0].RewardType', line 1, position 15.

Hi,
Thanks for this open-source project.
I am not familiar with JSON serialization. I tried to follow the instructions provided in the README file, but I encountered the error mentioned in the title. I will provide the relevant information below and hope someone can help me.


My env:

Windows 11
Unity 2022.3.17f1
Visual Studio 2022

My code:

using System;
using System.Collections.Generic;
using DysonCore.DynamicJson.PolymorphicParser;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using UnityEngine;
using System;
using System.IO;

public abstract class Reward
{
    // Can be used in conjunction with the [JsonProperty] attribute. 
    [TypifyingProperty]
    public abstract string RewardType { get; }
}


public class CurrencyReward : Reward
{
    [TypifyingProperty]
    // Tip: use the 'sealed' keyword if You don't intend 
    // to further inherit from this class and override the qualifying value.  
    public sealed override string RewardType => "currency";

    public int Amount { get; set; }
}

public class BadgeReward : Reward
{
    [TypifyingProperty]
    public sealed override string RewardType => "badge";

    public string BadgeId { get; set; }
}

public class generator : MonoBehaviour
{

    // Start is called before the first frame update
    void Start()
    {
        var settings = new JsonSerializerSettings();
        settings.Converters.Add(new PolymorphicConverter());

        var serializer = new JsonSerializer();
        serializer.Converters.Add(new PolymorphicConverter());



        List<Reward> rewards = new List<Reward>
        {
            new CurrencyReward{Amount = 100},
            new BadgeReward{BadgeId = "newbie_badge_01"}
        };

        string rewardsJson = JsonConvert.SerializeObject(rewards);
        Debug.Log("rewardsJson value: " + rewardsJson);
        // rewardsJson value: [{"RewardType":"currency","Amount":100},{"RewardType":"badge","BadgeId":"newbie_badge_01"}]
        List<Reward> deserializedRewards = JsonConvert.DeserializeObject<List<Reward>>(rewardsJson);
        // Note: The above has been tested with the [JsonConverter(typeof(PolymorphicConverter))] attribute applied to the Reward class.
    }

}

The full error message:

JsonSerializationException: Could not create an instance of type Reward. Type is an interface or abstract class and cannot be instantiated. Path '[0].RewardType', line 1, position 15.
Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateNewObject (Newtonsoft.Json.JsonReader reader, Newtonsoft.Json.Serialization.JsonObjectContract objectContract, Newtonsoft.Json.Serialization.JsonProperty containerMember, Newtonsoft.Json.Serialization.JsonProperty containerProperty, System.String id, System.Boolean& createdFromNonDefaultCreator) (at <761cf2a144514d2291a678c334d49e9b>:0)
Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject (Newtonsoft.Json.JsonReader reader, System.Type objectType, Newtonsoft.Json.Serialization.JsonContract contract, Newtonsoft.Json.Serialization.JsonProperty member, Newtonsoft.Json.Serialization.JsonContainerContract containerContract, Newtonsoft.Json.Serialization.JsonProperty containerMember, System.Object existingValue) (at <761cf2a144514d2291a678c334d49e9b>:0)
Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal (Newtonsoft.Json.JsonReader reader, System.Type objectType, Newtonsoft.Json.Serialization.JsonContract contract, Newtonsoft.Json.Serialization.JsonProperty member, Newtonsoft.Json.Serialization.JsonContainerContract containerContract, Newtonsoft.Json.Serialization.JsonProperty containerMember, System.Object existingValue) (at <761cf2a144514d2291a678c334d49e9b>:0)
Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateList (System.Collections.IList list, Newtonsoft.Json.JsonReader reader, Newtonsoft.Json.Serialization.JsonArrayContract contract, Newtonsoft.Json.Serialization.JsonProperty containerProperty, System.String id) (at <761cf2a144514d2291a678c334d49e9b>:0)
Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateList (Newtonsoft.Json.JsonReader reader, System.Type objectType, Newtonsoft.Json.Serialization.JsonContract contract, Newtonsoft.Json.Serialization.JsonProperty member, System.Object existingValue, System.String id) (at <761cf2a144514d2291a678c334d49e9b>:0)
Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal (Newtonsoft.Json.JsonReader reader, System.Type objectType, Newtonsoft.Json.Serialization.JsonContract contract, Newtonsoft.Json.Serialization.JsonProperty member, Newtonsoft.Json.Serialization.JsonContainerContract containerContract, Newtonsoft.Json.Serialization.JsonProperty containerMember, System.Object existingValue) (at <761cf2a144514d2291a678c334d49e9b>:0)
Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize (Newtonsoft.Json.JsonReader reader, System.Type objectType, System.Boolean checkAdditionalContent) (at <761cf2a144514d2291a678c334d49e9b>:0)
Newtonsoft.Json.JsonSerializer.DeserializeInternal (Newtonsoft.Json.JsonReader reader, System.Type objectType) (at <761cf2a144514d2291a678c334d49e9b>:0)
Newtonsoft.Json.JsonSerializer.Deserialize (Newtonsoft.Json.JsonReader reader, System.Type objectType) (at <761cf2a144514d2291a678c334d49e9b>:0)
Newtonsoft.Json.JsonConvert.DeserializeObject (System.String value, System.Type type, Newtonsoft.Json.JsonSerializerSettings settings) (at <761cf2a144514d2291a678c334d49e9b>:0)
Newtonsoft.Json.JsonConvert.DeserializeObject[T] (System.String value, Newtonsoft.Json.JsonSerializerSettings settings) (at <761cf2a144514d2291a678c334d49e9b>:0)
Newtonsoft.Json.JsonConvert.DeserializeObject[T] (System.String value) (at <761cf2a144514d2291a678c334d49e9b>:0)
generator.Start () (at Assets/script/generator.cs:60)

Screenshot 2024-06-29 004218

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.