I am automating how to serialize a class with [Serializable] attribute to Json in Unity.
I tried to get the FieldInfo by reflection and pass that value to parse it as Json, but it failed. Empty curly braces are output.
Problem Code
public static class SerializeHelper
{
public static void SerializeObjectField()
{
var fieldsInfo = GameInfo.Instance.GetType().GetFields(
BindingFlags.Public | BindingFlags.Instance);
foreach (var fieldInfo in fieldsInfo)
{
ToJson(fieldInfo);
}
}
private static void ToJson(FieldInfo fieldInfo)
{
var json = JsonUtility.ToJson(fieldInfo, true);
Debug.Log(json); // Empty curly braces are output
}
}
public class GameInfo : MonoBehaviour
{
// Sigleton Pattern
public Gold gold = new();
public int Gold
{
get => gold.Amount;
set => gold.Amount = value;
}
// ...
}
[Serializable]
public class Gold
{
[SerializeField] private int amount;
public int Amount
{
get => amount;
set => amount = value;
}
}
If I write it manually without using reflection, it outputs just fine.
Correct Code
// ...
var json = JsonUtility.ToJson(GameInfo.Instance.gold, true);
Debug.Log(json);
// ...
Output
{
"amount": 43 // Create a button that increments by 1 for every click
}
Can you please tell me what I am doing wrong when using reflection?
You are missing one call, property info gives you a description of a field/property, which is related to class definition but has no reference to your actual instance.
To actually get the value you need to call GetValue method on the property info
try replacing
var json = JsonUtility.ToJson(fieldInfo, true);
with something similar to:
var json = JsonUtility.ToJson(fieldInfo.GetValue(instanceReference), true);
A working example below:
public class ReflectionTest : MonoBehaviour
{
[System.Serializable]
public class TestClass
{
public string myString = "test";
}
public TestClass testClassInstance = new TestClass();
void OnValidate()
{
var fieldInfo = this.GetType().GetField("testClassInstance");
var value = fieldInfo.GetValue(this);
var valueString = JsonUtility.ToJson(value);
Debug.Log($"after serialization {valueString}");
}
}
Related
I am trying to find a specific value inside a string, and then check the characters which come after the value.
I got the first part and can find the values I need but want to check after every value I find if any of the ten characters in front of it in the string are numbers or not.
here is the first part I got so far:
public class Text_Value : MonoBehaviour
{
[SerializeField]
private TMP_Text Text;
public List<string> Values = new List<string>();
private int ChestCircumference;
private int BodyHeight;
private int HighHipCircumference;
private int BellyCircumference;
private int CharacterIndex = 0;
// Start is called before the first frame update
void Start()
{
StartCoroutine(GetValues());
}
// Update is called once per frame
void Update()
{
}
IEnumerator GetValues()
{
if (Text.text == "No Data")
{
yield return new WaitForSeconds(0.1f);
Debug.Log("Text Value Finder: Wating For Text Data");
StartCoroutine(GetValues());
}
else if (Text.text != "No Data")
{
foreach (string value in Values)
{
if (Text.text.Contains(value))
{
Debug.Log("Value Found: " + value);
}
else if (!Text.text.Contains(value))
{
Debug.Log("Missing Value: " + value);
}
}
}
}
}
I think I need to use a loop inside the foreach loop, one that will go over the next ten characters, but not sure how to do it.
An example of the string would be:
"Collar Circumference":39.102776,"Neck Base Circumference":42.982479"
As said you could use Regex.Matches like e.g.
var matches = Regex.Matches(Text.text, "\"(.*?)\":(.*?)(?=,|$)");
foreach(var match in matches)
{
var key = match.Groups[1];
var valueString = match.Groups[2];
if(float.TryParse(valueString, out var value))
{
Debug.Log($"Found match for {key} with value = {value}");
}
}
See Regex Example and explanation
However, if this is actually a JSON then you should rather use a proper c# representation and actually parse the JSON e.g. depending on the complexity of your JSON simply using the built-in JsonUtility
Assuming you have access to the JSON generation and can rather use names without spaces like
{
"ChestCircumference":12.34,
"BodyHeight":1.23,
"HighHipCircumference":0.23,
"BellyCircumference":0.34567,
"CharacterIndex": 3
}
In your case you could e.g. wrap your values like
// The class name doesn't matter in JSON
[Serializable]
public class Config
{
public float ChestCircumference;
public float BodyHeight;
public float HighHipCircumference;
public float BellyCircumference;
public int CharacterIndex = 0;
}
and then simply do
var config = JsonUtility.FromJson<Config>(jsonString);
Or if changing the JSON is not an option use Newtonsoft JSON like e.g.
{
"Chest Circumference":12.34,
"Body Height":1.23,
"High Hip Circumference":0.23,
"Belly Circumference":0.34567,
"Character Index": 3
}
Then
[Serializable]
public class Config
{
[JsonProperty ("Chest Circumference")]
public float ChestCircumference;
[JsonProperty ("Body Height")]
public float BodyHeight;
[JsonProperty ("High Hip Circumference")]
public float HighHipCircumference;
[JsonProperty ("Belly Circumference")]
public float BellyCircumference;
[JsonProperty ("Character Index")]
public int CharacterIndex = 0;
}
And then do
var config = JsonConvert.DeserializeObject<Config>(jsonString);
The easiest way to access specific values, is to parse the string and put it in a dictionary, where the name of the part is used as key:
string input =
"\"Collar Circumference\":39.102776,\"Neck Base Circumference\":42.982479";
var dict = input.Split(",\"")
.Select(p => p.TrimStart('"').Split("\":"))
.ToDictionary(x => x[0], x => Double.Parse(x[1]));
This test ...
double collar = dict["Collar Circumference"];
double neckBase = dict["Neck Base Circumference"];
Console.WriteLine(collar);
Console.WriteLine(neckBase);
foreach (var entry in dict) {
Console.WriteLine($"\"{entry.Key}\" = {entry.Value}");
}
... prints:
39.102776
42.982479
"Collar Circumference" = 39.102776
"Neck Base Circumference" = 42.982479
This assumes that all values can be expressed as double. If this is not the case, just keep them as string:
...
.ToDictionary(x => x[0], x => x[1]);
2 versions, both outputting the correct result:
Using .NET 5 System.Text.Json
This would work:
using System;
public class Program
{
public static void Main()
{
var jsonString = #"{ ""Collar Circumference"":39.5678,""NeckBase Circumference"":42.876}";
//parse it
var yourObject = System.Text.Json.JsonDocument.Parse(jsonString);
//retrieve the value
var circumReference = yourObject.RootElement.GetProperty("Collar Circumference");
Console.WriteLine(circumReference);
}
}
Using Newtonsoft version:
Install package: https://www.nuget.org/packages/Newtonsoft.Json/13.0.1
using System;
public class Program
{
public static void Main()
{
var jsonString = #"{ ""Collar Circumference"":39.5678,""NeckBase Circumference"":42.876}";
dynamic obj = Newtonsoft.Json.Linq.JObject.Parse(jsonString);
Console.WriteLine(obj["Collar Circumference"].ToString());
}
}
I am currently working on a solution that Deserializes the list of email from json array, Once I get the list of email objects, I am validating using another class then set into another list.
Below is the code for Deserialize:
static void Main(string[] args)
{
string jsonTest = "{\"Emails\":[\"testUser#gmail.com\",\"testUser2#gmail.com\"]}";
Class1 contact = JsonConvert.DeserializeObject<Class1>(jsonTest);
Console.WriteLine(contact.Emails[0]);
Console.WriteLine(contact.Emails[1]);
}
And, below is the class definition for Class1 for deserialize:
public class Class1
{
private readonly List<ValidateEmail> emailsobj;
public List<string> Emails
{
get
{
return emailsobj.Select(o => o.EmailAdd).ToList();
}
set
{
foreach (string email in value)
{
emailsobj.Add(new ValidateEmail(email));
}
}
}
}
And, below is the validate class:
public class ValidateEmail
{
public string EmailAdd { get; set; }
private bool Valid;
public ValidateEmail(string value)
{
try
{
MailAddress mail = new MailAddress(EmailAdd);
EmailAdd = value;
Valid = true;
}
catch (FormatException)
{
Valid = false;
}
}
}
Whenever I deserialize I am getting exception on line :
Console.WriteLine(contact.Emails[0]);
Newtonsoft.Json.JsonSerializationException: Error getting value from
'Emails' on 'JsonGenerator.Class1'. ---> System.ArgumentNullException:
Value cannot be null. (Parameter 'source')
It looks like the List<Email> never set. Any help on this, it would be much appreciated.
You have to set the below setting(Replace) if you are using Lists - ObjectCreationHandling.
Replace Always create new objects and enables the setter property for the Lists!
So after you initialize the new list in Class1 like below:
private readonly List<ValidateEmail> emailsobj = new List<ValidateEmail>();
Change the code to have setting:
var settings = new JsonSerializerSettings
{
ObjectCreationHandling = ObjectCreationHandling.Replace
};
Class1 contact = JsonConvert.DeserializeObject<Class1>(jsonTest, settings);
See the detailed explanation here !
So i wanted to store categories of data within my "player" class with static classes so that they are named variables and are callable by name. I recently found a solution for calling variables within a class by their string name to get and set them here: C# setting/getting the class properties by string name and i tried to call the static class via string by using "Type.GetType(pattern)": Get class by string value
I attempted to modify the object to also call the static class by a string but I am missing something because get and set doesn't work at all:
public class Player
{
//categories of variables within static classes
public static class Status { public static int health = 10, stamina = 10, mana = 10; };
public static class Immunity { public static bool fire = false, water = false, giantEnemyCrab = true; };
//paralell arrays
public string[] namelistStatus = { "health", "stamina", "mana" };
public string[] namelistImmunity = { "fire", "water", "giantEnemyCrab" };
//get and set Variables from nested classes
//('classname' is the name of the class,'propertyName' is the name of the variable)
public object this[string className, string propertyName]
{
//indirectly calls variables within static classes entirely via string
get
{
//i think the problem originates from "Type.GetType(className)" not being the correct way to call the static class by their string name
Type myType = Type.GetType(className);
PropertyInfo myPropInfo = myType.GetProperty(propertyName);
return myPropInfo.GetValue(this, null);
}
set
{
Type myType = Type.GetType(className);
PropertyInfo myPropInfo = myType.GetProperty(propertyName);
myPropInfo.SetValue(this, value, null);
}
}
//display variables
public void DisplayPlayerStatus()
{
for (int i = 0; i < namelistStatus.Length; i++)
{
Console.WriteLine(namelistStatus[i]+":"+this["Status", namelistStatus[i]]);
}
for (int i = 0; i < namelistStatus.Length; i++)
{
if (Convert.ToBoolean(this["Immunity", namelistStatus[i]]))
{
Console.WriteLine(namelistStatus[i] + " Immunity");
}
}
}
}
That was a simplification of the Player class I'm trying to organize with nested static classes, there are also some functions that are meant to set the variables in the status and immunity classes but here i only made an example function for 'getting' the nested static class variables via string but 'setting' doesn't work either.
Any suggestions of how i would be able to do this properly would be much appreciated.
Problems:
Your static inner classes are just that - nested Types inside your Player class - not properties of your Player class.
Your properties inside your static class are actually Fields.
You used the wrong name list for Immunities inside Player.DisplayPlayerStatus()
You could ( but you really should NOT , reconsider your design ) fix it like this
public class Player
{
public string[] namelistImmunity = { "fire", "water", "giantEnemyCrab" };
public string[] namelistStatus = { "health", "stamina", "mana" };
public object this[string className, string propertyName]
{
//indirectly calls variables within static classes entirely via string
get
{
var innerClass = GetType().GetNestedType(className);
var myFieldInfo = innerClass?.GetField(propertyName);
return myFieldInfo.GetValue(null);
}
set
{
var innerClass = GetType().GetNestedType(className);
var myFieldInfo = innerClass?.GetField(propertyName);
myFieldInfo?.SetValue(null, value);
}
}
public void DisplayPlayerStatus()
{
// why for and indexing - foreach is far easier
foreach (var s in namelistStatus)
Console.WriteLine(s + ":" + this["Status", s]);
// you used the wrong list of names here
foreach (var n in namelistImmunity)
if (Convert.ToBoolean(this["Immunity", n]))
Console.WriteLine(n + " Immunity");
}
public static class Immunity
{
// these are fields
public static bool fire = false,
water = false,
giantEnemyCrab = true;
};
public static class Status
{
// fields as well
public static int health = 10,
stamina = 10,
mana = 10;
};
}
Tested with:
using System;
internal class Program
{
public static void Main(string[] args)
{
Player p = new Player();
p.DisplayPlayerStatus();
Console.WriteLine();
p["Status", "health"] = 200;
p.DisplayPlayerStatus();
Console.ReadLine();
}
// the fixed stuff goes here
}
Output:
health:10
stamina:10
mana:10
giantEnemyCrab Immunity
health:200
stamina:10
mana:10
giantEnemyCrab Immunity
Your classes are currently very tighty coupled, if you add psyonicPower to your stats, you need to edit your Player, edit Player.Status, modify Player.namelistStatus.
You could autodiscover the static class fieldnames via reflection in a similar fashion like you access the field-values and get rid of both your namelistXXXX - but still, the design is bad.
I have the following code:
[Serializable]
public class CustomClass
{
public CustomClass()
{
this.Init();
}
public void Init()
{
foreach (PropertyInfo p in this.GetType().GetProperties())
{
DescriptionAttribute da = null;
DefaultValueAttribute dv = null;
foreach (Attribute attr in p.GetCustomAttributes(true))
{
if (attr is DescriptionAttribute)
{
da = (DescriptionAttribute) attr;
}
if (attr is DefaultValueAttribute)
{
dv = (DefaultValueAttribute) attr;
}
}
UInt32 value = 0;
if (da != null && !String.IsNullOrEmpty(da.Description))
{
value = Factory.Instance.SelectByCode(da.Description, 3);
}
if (dv != null && value == 0)
{
value = (UInt32) dv.Value;
}
p.SetValue(this, value, null);
}
}
private UInt32 name;
[Description("name")]
[DefaultValue(41)]
public UInt32 Name
{
get { return this.name; }
set { this.name = value; }
}
(30 more properties)
}
Now the weird thing is: when I try to serialize this class I will get an empty node CustomClass!
<CustomClass />
And when I remove Init from the constructor it works as expected! I will get the full xml representation of the class but ofcourse without values (all with value 0).
<CustomClass>
<Name>0</Name>
...
</CustomClass>
Also, when I comment out the body of Init, I will get the same as above (the one with default values)
I've tried it with a public method, with a Helper class everything, but it does not work. That is, instead of the expected:
<CustomClass>
<Name>15</Name>
...
</CustomClass>
I will get
<CustomClass />
It seems when I use reflection in this class, serialization is not possible.
Or to summarize: when I call Init or when I fill my properties with reflection -> Serialization fails, when I remove this code part -> Serialization works but of course without my values.
Is this true? And does somebody know an alternative for my solution?
It should automatically get something from the database based on the Description and when this returns nothing it falls back to the DefaultValue...
PS1: I am using the XmlSerializer
PS2: When I set a breakpoint before the serialization, I can see that all the properties are filled with the good values (like 71, 72 etc).
Now the weird thing is: when I try to serialize this class I will get an empty node CustomClass!
XmlSerializer uses DefaultValue to decide which values to serialize - if it matches the default value, it doesn't store it. This approach is consistent with similar models such as data-binding / model-binding.
Frankly, I would say that in this case both DefaultValueAttribute and DescriptionAttribute are poor choices. Write your own - perhaps EavInitAttribute - then use something like:
[EavInit(41, "name")]
public uint Name {get;set;}
Note that there are other ways of controlling this conditional serialization - you could write a method like:
public bool ShouldSerializeName() { return true; }
which will also work to convince it to write the value (this is another pattern recognised by various serialization and data-binding APIs) - but frankly this is even more work (it is per-property, and needs to be public, so it makes a mess of the API).
Finally, I would say that hitting the database multiple times (once per property) for every new object construction is very expensive - especially since many of those values are likely to be assigned values in a moment anyway (so looking them up is wasted effort). I would put a lot of thought into making this both "lazy" and "cached" if it was me.
An example of a lazy and "sparse" implementation:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Xml.Serialization;
static class Program
{
static void Main()
{
var obj = new CustomClass();
Console.WriteLine(obj.Name);
// show it working via XmlSerializer
new XmlSerializer(obj.GetType()).Serialize(Console.Out, obj);
}
}
public class CustomClass : EavBase
{
[EavInit(42, "name")]
public uint Name
{
get { return GetEav(); }
set { SetEav(value); }
}
}
public abstract class EavBase
{
private Dictionary<string, uint> values;
protected uint GetEav([CallerMemberName] string propertyName = null)
{
if (values == null) values = new Dictionary<string, uint>();
uint value;
if (!values.TryGetValue(propertyName, out value))
{
value = 0;
var prop = GetType().GetProperty(propertyName);
if (prop != null)
{
var attrib = (EavInitAttribute)Attribute.GetCustomAttribute(
prop, typeof(EavInitAttribute));
if (attrib != null)
{
value = attrib.DefaultValue;
if (!string.IsNullOrEmpty(attrib.Key))
{
value = LookupDefaultValueFromDatabase(attrib.Key);
}
}
}
values.Add(propertyName, value);
}
return value;
}
protected void SetEav(uint value, [CallerMemberName] string propertyName = null)
{
(values ?? (values = new Dictionary<string, uint>()))[propertyName] = value;
}
private static uint LookupDefaultValueFromDatabase(string key)
{
// TODO: real code here
switch (key)
{
case "name":
return 7;
default:
return 234;
}
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
protected class EavInitAttribute : Attribute
{
public uint DefaultValue { get; private set; }
public string Key { get; private set; }
public EavInitAttribute(uint defaultValue) : this(defaultValue, "") { }
public EavInitAttribute(string key) : this(0, key) { }
public EavInitAttribute(uint defaultValue, string key)
{
DefaultValue = defaultValue;
Key = key ?? "";
}
}
}
There seems to be a bug / inconsistency in the Microsoft XmlSerializer: If you have a property marked with a System.ComponentModel.DefaultValue attribute, this does not get serialized. Fair enough - this could be seen as an expected behavior.
The problem is the same attribute is not respected when deserializing. The code below illustrates the issue.
Question is how could I bypass this? I have potentially hundreds of business classes with default values used in the UI tier (Views), so default value initialization in constructor is not an option. It has to be something generic. I could create a completely new default attribute, but it seems like duplicate work. Do you see a way to override the XmlSerializer behavior or should I use just another serializer that does the job better?
The example code:
public class DefaultValueTestClass
{
[System.ComponentModel.DefaultValue(10000)]
public int Foo { get; set; }
}
[TestMethod]
public void SimpleDefaultValueTest()
{
// Create object and set the property value TO THE DEFAULT
var before = new DefaultValueTestClass();
before.Foo = 10000;
// Serialize => xml
var serializer = new System.Xml.Serialization.XmlSerializer(typeof(DefaultValueTestClass));
string xml;
using (var stream = new System.IO.StringWriter())
{
serializer.Serialize(stream, before);
xml = stream.ToString();
}
// Deserialize the same object
DefaultValueTestClass after;
using (var reader = new System.IO.StringReader(xml))
{
after = (DefaultValueTestClass)serializer.Deserialize(reader);
}
// before.Foo = 10000
// after.Foo = 0
Assert.AreEqual(before.Foo, after.Foo);
}
It is your job to implement the defaults; [DefaultValue] merely says "this is the default, you don't need to worry about this" - it doesn't apply it. This applies not just to XmlSerializer, but to the core System.ComponentModel API to which [DefaultValue] belongs (which drives things like the bold / not-bold in PropertyGrid, etc)
Basically:
public class DefaultValueTestClass
{
public DefaultValueTestClass()
{
Foo = 10000;
}
[DefaultValue(10000)]
public int Foo { get; set; }
}
will work in the way you expect. If you want it to serialize whether or not it is that particular value, then the correct implementations is:
public class DefaultValueTestClass
{
public DefaultValueTestClass()
{
Foo = 10000;
}
public int Foo { get; set; }
}
If you want to preserve the [DefaultValue], but want it to always serialize, then:
public class DefaultValueTestClass
{
[DefaultValue(10000)]
public int Foo { get; set; }
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public bool ShouldSerializeFoo() { return true; }
}
Where ShouldSerialize* is another pattern from System.ComponentModel that is recognised by several serializers.
And here's some UI code to show that XmlSerializer is actually doing exactly the same things that the UI code (built on System.ComponentModel) has always done:
using System.ComponentModel;
using System.Windows.Forms;
static class Program
{
[System.STAThread]
static void Main()
{
Application.EnableVisualStyles();
using (var form = new Form())
using (var grid = new PropertyGrid())
{
grid.Dock = DockStyle.Fill;
var obj = new DefaultValueTestClass
{ // TODO - try with other numbers to
// see bold / not bold
Foo = 10000
};
// note in the grid the value is shown not-bold; that is
// because System.ComponentModel is saying
// "this property doesn't need to be serialized"
// - or to show it more explicitly:
var prop = TypeDescriptor.GetProperties(obj)["Foo"];
bool shouldSerialize = prop.ShouldSerializeValue(obj);
// ^^^ false, because of the DefaultValueAttribute
form.Text = shouldSerialize.ToString(); // win title
grid.SelectedObject = obj;
form.Controls.Add(grid);
Application.Run(form);
}
}
}
public class DefaultValueTestClass
{
[System.ComponentModel.DefaultValue(10000)]
public int Foo { get; set; }
}