Kaynağa Gözat

Easier custom formatting for value types

Brad Robinson 11 yıl önce
ebeveyn
işleme
17e8ef453f
3 değiştirilmiş dosya ile 513 ekleme ve 300 silme
  1. 398 276
      PetaJson.cs
  2. 69 23
      TestCases/TestCustomFormat.cs
  3. 46 1
      readme.md

+ 398 - 276
PetaJson.cs

@@ -558,13 +558,28 @@ namespace PetaJson
 
             static Func<IJsonReader, Type, object> ResolveParser(Type type)
             {
-				// Run the object's static constructors
-				Activator.CreateInstance(type);
-
-				// See if it registered
-				Func<IJsonReader, Type, object> parser;
-				if (_parsers.TryGetValue(type, out parser))
-					return parser;
+                // See if the Type has a static parser method - T ParseJson(IJsonReader)
+                var parseJson = ReflectionInfo.FindParseJson(type);
+                if (parseJson != null)
+                {
+                    if (parseJson.GetParameters()[0].ParameterType == typeof(IJsonReader))
+                    {
+                        return (r, t) => parseJson.Invoke(null, new Object[] { r });
+                    }
+                    else
+                    {
+                        return (r, t) =>
+                        {
+                            if (r.GetLiteralKind() == LiteralKind.String)
+                            {
+                                var o = parseJson.Invoke(null, new Object[] { r.GetLiteralString() });
+                                r.NextToken();
+                                return o;
+                            }
+                            throw new InvalidDataException(string.Format("Expected string literal for type {0}", type.FullName));
+                        };
+                    }
+                }
 
                 return (r, t) =>
                 {
@@ -984,6 +999,16 @@ namespace PetaJson
 
             static Action<IJsonWriter, object> ResolveFormatter(Type type)
             {
+                // Try `void FormatJson(IJsonWriter)`
+                var formatJson = ReflectionInfo.FindFormatJson(type);
+                if (formatJson != null)
+                {
+                    if (formatJson.ReturnType==typeof(void))
+                        return (w, obj) => formatJson.Invoke(obj, new Object[] { w });
+                    if (formatJson.ReturnType == typeof(string))
+                        return (w, obj) => w.WriteStringLiteral((string)formatJson.Invoke(obj, new Object[] { }));
+                }
+
                 var ri = ReflectionInfo.GetReflectionInfo(type);
                 if (ri != null)
                     return ri.Write;
@@ -1308,6 +1333,38 @@ namespace PetaJson
             // Cache of these ReflectionInfos's
             static ThreadSafeCache<Type, ReflectionInfo> _cache = new ThreadSafeCache<Type, ReflectionInfo>();
 
+            public static MethodInfo FindFormatJson(Type type)
+            {
+                if (type.IsValueType)
+                {
+                    // Try `void FormatJson(IJsonWriter)`
+                    var formatJson = type.GetMethod("FormatJson", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(IJsonWriter) }, null);
+                    if (formatJson != null && formatJson.ReturnType == typeof(void))
+                        return formatJson;
+
+                    // Try `string FormatJson()`
+                    formatJson = type.GetMethod("FormatJson", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { }, null);
+                    if (formatJson != null && formatJson.ReturnType == typeof(string))
+                        return formatJson;
+                }
+                return null;
+            }
+
+            public static MethodInfo FindParseJson(Type type)
+            {
+                // Try `T ParseJson(IJsonReader)`
+                var parseJson = type.GetMethod("ParseJson", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, null, new Type[] { typeof(IJsonReader) }, null);
+                if (parseJson != null && parseJson.ReturnType == type)
+                    return parseJson;
+
+                // Try `T ParseJson(string)`
+                parseJson = type.GetMethod("ParseJson", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, null, new Type[] { typeof(string) }, null);
+                if (parseJson != null && parseJson.ReturnType == type)
+                    return parseJson;
+
+                return null;
+            }
+
             // Write one of these types
             public void Write(IJsonWriter w, object val)
             {
@@ -1523,9 +1580,6 @@ namespace PetaJson
                     _lock.ExitReadLock();
                 }
 
-				// Create it while not holding lock
-				TValue valNew = createIt();
-
                 // Nope, take lock and try again
                 _lock.EnterWriteLock();
                 try
@@ -1535,7 +1589,7 @@ namespace PetaJson
                     if (!_map.TryGetValue(key, out val))
                     {
 						// Store the new one
-						val = valNew;
+						val = createIt();
                         _map[key] = val;
                     }
                     return val;
@@ -2169,258 +2223,285 @@ namespace PetaJson
             // Generates a function that when passed an object of specified type, renders it to an IJsonReader
             public static Action<IJsonWriter, object> MakeFormatter(Type type)
             {
-                // Get the reflection info for this type
-                var ri = ReflectionInfo.GetReflectionInfo(type);
-                if (ri == null)
-                    return null;
+                var formatJson = ReflectionInfo.FindFormatJson(type);
+                if (formatJson != null)
+                {
+                    var method = new DynamicMethod("invoke_formatJson", null, new Type[] { typeof(IJsonWriter), typeof(Object) }, true);
+                    var il = method.GetILGenerator();
+                    if (formatJson.ReturnType == typeof(string))
+                    {
+                        // w.WriteStringLiteral(o.FormatJson())
+                        il.Emit(OpCodes.Ldarg_0); 
+                        il.Emit(OpCodes.Ldarg_1);
+                        il.Emit(OpCodes.Unbox, type);
+                        il.Emit(OpCodes.Call, formatJson);
+                        il.Emit(OpCodes.Callvirt, typeof(IJsonWriter).GetMethod("WriteStringLiteral"));
+                    }
+                    else
+                    {
+                        // o.FormatJson(w);
+                        il.Emit(OpCodes.Ldarg_1);
+                        il.Emit(type.IsValueType ? OpCodes.Unbox : OpCodes.Castclass, type);
+                        il.Emit(OpCodes.Ldarg_0);
+                        il.Emit(type.IsValueType ? OpCodes.Call : OpCodes.Callvirt, formatJson);
+                    }
+                    il.Emit(OpCodes.Ret);
+                    return (Action<IJsonWriter, object>)method.CreateDelegate(typeof(Action<IJsonWriter, object>));
+                }
+                else
+                {
+                    // Get the reflection info for this type
+                    var ri = ReflectionInfo.GetReflectionInfo(type);
+                    if (ri == null)
+                        return null;
 
-                // Create a dynamic method that can do the work
-                var method = new DynamicMethod("dynamic_formatter", null, new Type[] { typeof(IJsonWriter), typeof(object) }, true);
-                var il = method.GetILGenerator();
+                    // Create a dynamic method that can do the work
+                    var method = new DynamicMethod("dynamic_formatter", null, new Type[] { typeof(IJsonWriter), typeof(object) }, true);
+                    var il = method.GetILGenerator();
 
-                // Cast/unbox the target object and store in local variable
-                var locTypedObj = il.DeclareLocal(type);
-                il.Emit(OpCodes.Ldarg_1);
-                il.Emit(type.IsValueType ? OpCodes.Unbox_Any : OpCodes.Castclass, type);
-                il.Emit(OpCodes.Stloc, locTypedObj);
+                    // Cast/unbox the target object and store in local variable
+                    var locTypedObj = il.DeclareLocal(type);
+                    il.Emit(OpCodes.Ldarg_1);
+                    il.Emit(type.IsValueType ? OpCodes.Unbox_Any : OpCodes.Castclass, type);
+                    il.Emit(OpCodes.Stloc, locTypedObj);
 
-                // Get Invariant CultureInfo (since we'll probably be needing this)
-                var locInvariant = il.DeclareLocal(typeof(IFormatProvider));
-                il.Emit(OpCodes.Call, typeof(CultureInfo).GetProperty("InvariantCulture").GetGetMethod());
-                il.Emit(OpCodes.Stloc, locInvariant);
+                    // Get Invariant CultureInfo (since we'll probably be needing this)
+                    var locInvariant = il.DeclareLocal(typeof(IFormatProvider));
+                    il.Emit(OpCodes.Call, typeof(CultureInfo).GetProperty("InvariantCulture").GetGetMethod());
+                    il.Emit(OpCodes.Stloc, locInvariant);
 
-                // These are the types we'll call .ToString(Culture.InvariantCulture) on
-                var toStringTypes = new Type[] { 
+                    // These are the types we'll call .ToString(Culture.InvariantCulture) on
+                    var toStringTypes = new Type[] { 
                     typeof(int), typeof(uint), typeof(long), typeof(ulong), 
                     typeof(short), typeof(ushort), typeof(decimal), 
                     typeof(byte), typeof(sbyte)
                 };
 
-                // Theses types we also generate for
-                var otherSupportedTypes = new Type[] {
+                    // Theses types we also generate for
+                    var otherSupportedTypes = new Type[] {
                     typeof(double), typeof(float), typeof(string), typeof(char)
                 };
 
-                // Call IJsonWriting if implemented
-                if (typeof(IJsonWriting).IsAssignableFrom(type))
-                {
-                    if (type.IsValueType)
+                    // Call IJsonWriting if implemented
+                    if (typeof(IJsonWriting).IsAssignableFrom(type))
                     {
-                        il.Emit(OpCodes.Ldloca, locTypedObj);
-                        il.Emit(OpCodes.Ldarg_0);
-                        il.Emit(OpCodes.Call, type.GetInterfaceMap(typeof(IJsonWriting)).TargetMethods[0]);
-                    }
-                    else
-                    {
-                        il.Emit(OpCodes.Ldloc, locTypedObj);
-                        il.Emit(OpCodes.Castclass, typeof(IJsonWriting));
-                        il.Emit(OpCodes.Ldarg_0);
-                        il.Emit(OpCodes.Callvirt, typeof(IJsonWriting).GetMethod("OnJsonWriting", new Type[] { typeof(IJsonWriter) }));
+                        if (type.IsValueType)
+                        {
+                            il.Emit(OpCodes.Ldloca, locTypedObj);
+                            il.Emit(OpCodes.Ldarg_0);
+                            il.Emit(OpCodes.Call, type.GetInterfaceMap(typeof(IJsonWriting)).TargetMethods[0]);
+                        }
+                        else
+                        {
+                            il.Emit(OpCodes.Ldloc, locTypedObj);
+                            il.Emit(OpCodes.Castclass, typeof(IJsonWriting));
+                            il.Emit(OpCodes.Ldarg_0);
+                            il.Emit(OpCodes.Callvirt, typeof(IJsonWriting).GetMethod("OnJsonWriting", new Type[] { typeof(IJsonWriter) }));
+                        }
                     }
-                }
 
-                // Process all members
-                foreach (var m in ri.Members)
-                {
-                    // Ignore write only properties
-                    var pi = m.Member as PropertyInfo;
-                    if (pi != null && pi.GetGetMethod() == null)
+                    // Process all members
+                    foreach (var m in ri.Members)
                     {
-                        continue;
-                    }
-
-                    // Write the Json key
-                    il.Emit(OpCodes.Ldarg_0);
-                    il.Emit(OpCodes.Ldstr, m.JsonKey);
-                    il.Emit(OpCodes.Callvirt, typeof(IJsonWriter).GetMethod("WriteKeyNoEscaping", new Type[] { typeof(string) }));
-
-                    // Load the writer
-                    il.Emit(OpCodes.Ldarg_0);
+                        // Ignore write only properties
+                        var pi = m.Member as PropertyInfo;
+                        if (pi != null && pi.GetGetMethod() == null)
+                        {
+                            continue;
+                        }
 
-                    // Get the member type
-                    var memberType = m.MemberType;
+                        // Write the Json key
+                        il.Emit(OpCodes.Ldarg_0);
+                        il.Emit(OpCodes.Ldstr, m.JsonKey);
+                        il.Emit(OpCodes.Callvirt, typeof(IJsonWriter).GetMethod("WriteKeyNoEscaping", new Type[] { typeof(string) }));
 
-                    // Load the target object
-                    if (type.IsValueType)
-                    {
-                        il.Emit(OpCodes.Ldloca, locTypedObj);
-                    }
-                    else
-                    {
-                        il.Emit(OpCodes.Ldloc, locTypedObj);
-                    }
+                        // Load the writer
+                        il.Emit(OpCodes.Ldarg_0);
 
-                    // Work out if we need the value or it's address on the stack
-                    bool NeedValueAddress = (memberType.IsValueType && (toStringTypes.Contains(memberType) || otherSupportedTypes.Contains(memberType)));
-                    if (Nullable.GetUnderlyingType(memberType) != null)
-                    {
-                        NeedValueAddress = true;
-                    }
+                        // Get the member type
+                        var memberType = m.MemberType;
 
-                    // Property?
-                    if (pi != null)
-                    {
-                        // Call property's get method
+                        // Load the target object
                         if (type.IsValueType)
-                            il.Emit(OpCodes.Call, pi.GetGetMethod());
+                        {
+                            il.Emit(OpCodes.Ldloca, locTypedObj);
+                        }
                         else
-                            il.Emit(OpCodes.Callvirt, pi.GetGetMethod());
+                        {
+                            il.Emit(OpCodes.Ldloc, locTypedObj);
+                        }
 
-                        // If we need the address then store in a local and take it's address
-                        if (NeedValueAddress)
+                        // Work out if we need the value or it's address on the stack
+                        bool NeedValueAddress = (memberType.IsValueType && (toStringTypes.Contains(memberType) || otherSupportedTypes.Contains(memberType)));
+                        if (Nullable.GetUnderlyingType(memberType) != null)
                         {
-                            var locTemp = il.DeclareLocal(memberType);
-                            il.Emit(OpCodes.Stloc, locTemp);
-                            il.Emit(OpCodes.Ldloca, locTemp);
+                            NeedValueAddress = true;
                         }
-                    }
 
-                    // Field?
-                    var fi = m.Member as FieldInfo;
-                    if (fi != null)
-                    {
-                        if (NeedValueAddress)
+                        // Property?
+                        if (pi != null)
                         {
-                            il.Emit(OpCodes.Ldflda, fi);
+                            // Call property's get method
+                            if (type.IsValueType)
+                                il.Emit(OpCodes.Call, pi.GetGetMethod());
+                            else
+                                il.Emit(OpCodes.Callvirt, pi.GetGetMethod());
+
+                            // If we need the address then store in a local and take it's address
+                            if (NeedValueAddress)
+                            {
+                                var locTemp = il.DeclareLocal(memberType);
+                                il.Emit(OpCodes.Stloc, locTemp);
+                                il.Emit(OpCodes.Ldloca, locTemp);
+                            }
                         }
-                        else
+
+                        // Field?
+                        var fi = m.Member as FieldInfo;
+                        if (fi != null)
                         {
-                            il.Emit(OpCodes.Ldfld, fi);
+                            if (NeedValueAddress)
+                            {
+                                il.Emit(OpCodes.Ldflda, fi);
+                            }
+                            else
+                            {
+                                il.Emit(OpCodes.Ldfld, fi);
+                            }
                         }
-                    }
 
-                    Label? lblFinished = null;
+                        Label? lblFinished = null;
 
-                    // Is it a nullable type?
-                    var typeUnderlying = Nullable.GetUnderlyingType(memberType);
-                    if (typeUnderlying != null)
-                    {
-                        // Duplicate the address so we can call get_HasValue() and then get_Value()
-                        il.Emit(OpCodes.Dup);
+                        // Is it a nullable type?
+                        var typeUnderlying = Nullable.GetUnderlyingType(memberType);
+                        if (typeUnderlying != null)
+                        {
+                            // Duplicate the address so we can call get_HasValue() and then get_Value()
+                            il.Emit(OpCodes.Dup);
 
-                        // Define some labels
-                        var lblHasValue = il.DefineLabel();
-                        lblFinished = il.DefineLabel();
+                            // Define some labels
+                            var lblHasValue = il.DefineLabel();
+                            lblFinished = il.DefineLabel();
 
-                        // Call has_Value
-                        il.Emit(OpCodes.Call, memberType.GetProperty("HasValue").GetGetMethod());
-                        il.Emit(OpCodes.Brtrue, lblHasValue);
+                            // Call has_Value
+                            il.Emit(OpCodes.Call, memberType.GetProperty("HasValue").GetGetMethod());
+                            il.Emit(OpCodes.Brtrue, lblHasValue);
 
-                        // No value, write "null:
-                        il.Emit(OpCodes.Pop);
-                        il.Emit(OpCodes.Ldstr, "null");
-                        il.Emit(OpCodes.Callvirt, typeof(IJsonWriter).GetMethod("WriteRaw", new Type[] { typeof(string) }));
-                        il.Emit(OpCodes.Br_S, lblFinished.Value);
+                            // No value, write "null:
+                            il.Emit(OpCodes.Pop);
+                            il.Emit(OpCodes.Ldstr, "null");
+                            il.Emit(OpCodes.Callvirt, typeof(IJsonWriter).GetMethod("WriteRaw", new Type[] { typeof(string) }));
+                            il.Emit(OpCodes.Br_S, lblFinished.Value);
 
-                        // Get it's value
-                        il.MarkLabel(lblHasValue);
-                        il.Emit(OpCodes.Call, memberType.GetProperty("Value").GetGetMethod());
+                            // Get it's value
+                            il.MarkLabel(lblHasValue);
+                            il.Emit(OpCodes.Call, memberType.GetProperty("Value").GetGetMethod());
 
-                        // Switch to the underlying type from here on
-                        memberType = typeUnderlying;
-                        NeedValueAddress = (memberType.IsValueType && (toStringTypes.Contains(memberType) || otherSupportedTypes.Contains(memberType)));
+                            // Switch to the underlying type from here on
+                            memberType = typeUnderlying;
+                            NeedValueAddress = (memberType.IsValueType && (toStringTypes.Contains(memberType) || otherSupportedTypes.Contains(memberType)));
 
-                        // Work out again if we need the address of the value
-                        if (NeedValueAddress)
-                        {
-                            var locTemp = il.DeclareLocal(memberType);
-                            il.Emit(OpCodes.Stloc, locTemp);
-                            il.Emit(OpCodes.Ldloca, locTemp);
+                            // Work out again if we need the address of the value
+                            if (NeedValueAddress)
+                            {
+                                var locTemp = il.DeclareLocal(memberType);
+                                il.Emit(OpCodes.Stloc, locTemp);
+                                il.Emit(OpCodes.Ldloca, locTemp);
+                            }
                         }
-                    }
 
-                    // ToString()
-                    if (toStringTypes.Contains(memberType))
-                    {
-                        // Convert to string
-                        il.Emit(OpCodes.Ldloc, locInvariant);
-                        il.Emit(OpCodes.Call, memberType.GetMethod("ToString", new Type[] { typeof(IFormatProvider) }));
-                        il.Emit(OpCodes.Callvirt, typeof(IJsonWriter).GetMethod("WriteRaw", new Type[] { typeof(string) }));
-                    }
+                        // ToString()
+                        if (toStringTypes.Contains(memberType))
+                        {
+                            // Convert to string
+                            il.Emit(OpCodes.Ldloc, locInvariant);
+                            il.Emit(OpCodes.Call, memberType.GetMethod("ToString", new Type[] { typeof(IFormatProvider) }));
+                            il.Emit(OpCodes.Callvirt, typeof(IJsonWriter).GetMethod("WriteRaw", new Type[] { typeof(string) }));
+                        }
 
-                    // ToString("R")
-                    else if (memberType == typeof(float) || memberType == typeof(double))
-                    {
-                        il.Emit(OpCodes.Ldstr, "R");
-                        il.Emit(OpCodes.Ldloc, locInvariant);
-                        il.Emit(OpCodes.Call, memberType.GetMethod("ToString", new Type[] { typeof(string), typeof(IFormatProvider) }));
-                        il.Emit(OpCodes.Callvirt, typeof(IJsonWriter).GetMethod("WriteRaw", new Type[] { typeof(string) }));
-                    }
+                        // ToString("R")
+                        else if (memberType == typeof(float) || memberType == typeof(double))
+                        {
+                            il.Emit(OpCodes.Ldstr, "R");
+                            il.Emit(OpCodes.Ldloc, locInvariant);
+                            il.Emit(OpCodes.Call, memberType.GetMethod("ToString", new Type[] { typeof(string), typeof(IFormatProvider) }));
+                            il.Emit(OpCodes.Callvirt, typeof(IJsonWriter).GetMethod("WriteRaw", new Type[] { typeof(string) }));
+                        }
 
-                    // String?
-                    else if (memberType == typeof(string))
-                    {
-                        il.Emit(OpCodes.Callvirt, typeof(IJsonWriter).GetMethod("WriteStringLiteral", new Type[] { typeof(string) }));
-                    }
+                        // String?
+                        else if (memberType == typeof(string))
+                        {
+                            il.Emit(OpCodes.Callvirt, typeof(IJsonWriter).GetMethod("WriteStringLiteral", new Type[] { typeof(string) }));
+                        }
 
-                    // Char?
-                    else if (memberType == typeof(char))
-                    {
-                        il.Emit(OpCodes.Call, memberType.GetMethod("ToString", new Type[] { }));
-                        il.Emit(OpCodes.Callvirt, typeof(IJsonWriter).GetMethod("WriteStringLiteral", new Type[] { typeof(string) }));
-                    }
+                        // Char?
+                        else if (memberType == typeof(char))
+                        {
+                            il.Emit(OpCodes.Call, memberType.GetMethod("ToString", new Type[] { }));
+                            il.Emit(OpCodes.Callvirt, typeof(IJsonWriter).GetMethod("WriteStringLiteral", new Type[] { typeof(string) }));
+                        }
 
-                    // Bool?
-                    else if (memberType == typeof(bool))
-                    {
-                        var lblTrue = il.DefineLabel();
-                        var lblCont = il.DefineLabel();
-                        il.Emit(OpCodes.Brtrue_S, lblTrue);
-                        il.Emit(OpCodes.Ldstr, "false");
-                        il.Emit(OpCodes.Br_S, lblCont);
-                        il.MarkLabel(lblTrue);
-                        il.Emit(OpCodes.Ldstr, "true");
-                        il.MarkLabel(lblCont);
-                        il.Emit(OpCodes.Callvirt, typeof(IJsonWriter).GetMethod("WriteRaw", new Type[] { typeof(string) }));
-                    }
+                        // Bool?
+                        else if (memberType == typeof(bool))
+                        {
+                            var lblTrue = il.DefineLabel();
+                            var lblCont = il.DefineLabel();
+                            il.Emit(OpCodes.Brtrue_S, lblTrue);
+                            il.Emit(OpCodes.Ldstr, "false");
+                            il.Emit(OpCodes.Br_S, lblCont);
+                            il.MarkLabel(lblTrue);
+                            il.Emit(OpCodes.Ldstr, "true");
+                            il.MarkLabel(lblCont);
+                            il.Emit(OpCodes.Callvirt, typeof(IJsonWriter).GetMethod("WriteRaw", new Type[] { typeof(string) }));
+                        }
 
-                    // NB: We don't support DateTime as it's format can be changed
+                        // NB: We don't support DateTime as it's format can be changed
 
-                    else
-                    {
-                        // Unsupported type, pass through
-                        if (memberType.IsValueType)
+                        else
                         {
-                            il.Emit(OpCodes.Box, memberType);
+                            // Unsupported type, pass through
+                            if (memberType.IsValueType)
+                            {
+                                il.Emit(OpCodes.Box, memberType);
+                            }
+                            il.Emit(OpCodes.Callvirt, typeof(IJsonWriter).GetMethod("WriteValue", new Type[] { typeof(object) }));
                         }
-                        il.Emit(OpCodes.Callvirt, typeof(IJsonWriter).GetMethod("WriteValue", new Type[] { typeof(object) }));
-                    }
-
-                    if (lblFinished.HasValue)
-                        il.MarkLabel(lblFinished.Value);
-                }
 
-                // Call IJsonWritten
-                if (typeof(IJsonWritten).IsAssignableFrom(type))
-                {
-                    if (type.IsValueType)
-                    {
-                        il.Emit(OpCodes.Ldloca, locTypedObj);
-                        il.Emit(OpCodes.Ldarg_0);
-                        il.Emit(OpCodes.Call, type.GetInterfaceMap(typeof(IJsonWritten)).TargetMethods[0]);
+                        if (lblFinished.HasValue)
+                            il.MarkLabel(lblFinished.Value);
                     }
-                    else
+
+                    // Call IJsonWritten
+                    if (typeof(IJsonWritten).IsAssignableFrom(type))
                     {
-                        il.Emit(OpCodes.Ldloc, locTypedObj);
-                        il.Emit(OpCodes.Castclass, typeof(IJsonWriting));
-                        il.Emit(OpCodes.Ldarg_0);
-                        il.Emit(OpCodes.Callvirt, typeof(IJsonWriting).GetMethod("OnJsonWritten", new Type[] { typeof(IJsonWriter) }));
+                        if (type.IsValueType)
+                        {
+                            il.Emit(OpCodes.Ldloca, locTypedObj);
+                            il.Emit(OpCodes.Ldarg_0);
+                            il.Emit(OpCodes.Call, type.GetInterfaceMap(typeof(IJsonWritten)).TargetMethods[0]);
+                        }
+                        else
+                        {
+                            il.Emit(OpCodes.Ldloc, locTypedObj);
+                            il.Emit(OpCodes.Castclass, typeof(IJsonWriting));
+                            il.Emit(OpCodes.Ldarg_0);
+                            il.Emit(OpCodes.Callvirt, typeof(IJsonWriting).GetMethod("OnJsonWritten", new Type[] { typeof(IJsonWriter) }));
+                        }
                     }
-                }
 
-                // Done!
-                il.Emit(OpCodes.Ret);
-                var impl = (Action<IJsonWriter, object>)method.CreateDelegate(typeof(Action<IJsonWriter, object>));
+                    // Done!
+                    il.Emit(OpCodes.Ret);
+                    var impl = (Action<IJsonWriter, object>)method.CreateDelegate(typeof(Action<IJsonWriter, object>));
 
-                // Wrap it in a call to WriteDictionary
-                return (w, obj) =>
-                {
-                    w.WriteDictionary(() =>
+                    // Wrap it in a call to WriteDictionary
+                    return (w, obj) =>
                     {
-                        impl(w, obj);
-                    });
-                };
-
+                        w.WriteDictionary(() =>
+                        {
+                            impl(w, obj);
+                        });
+                    };
+                }
             }
 
             // Pseudo box lets us pass a value type by reference.  Used during 
@@ -2442,94 +2523,135 @@ namespace PetaJson
             {
                 System.Diagnostics.Debug.Assert(type.IsValueType);
 
-                // Get the reflection info for this type
-                var ri = ReflectionInfo.GetReflectionInfo(type);
-                if (ri == null)
-                    return null;
+                // ParseJson method?
+                var parseJson = ReflectionInfo.FindParseJson(type);
+                if (parseJson != null)
+                {
+                    if (parseJson.GetParameters()[0].ParameterType == typeof(IJsonReader))
+                    {
+                        var method = new DynamicMethod("invoke_ParseJson", typeof(Object), new Type[] { typeof(IJsonReader), typeof(Type) }, true);
+                        var il = method.GetILGenerator();
 
-                // We'll create setters for each property/field
-                var setters = new Dictionary<string, Action<IJsonReader, object>>();
+                        il.Emit(OpCodes.Ldarg_0);
+                        il.Emit(OpCodes.Call, parseJson);
+                        il.Emit(OpCodes.Box, type);
+                        il.Emit(OpCodes.Ret);
+                        return (Func<IJsonReader,Type,object>)method.CreateDelegate(typeof(Func<IJsonReader,Type,object>));
+                    }
+                    else
+                    {
+                        var method = new DynamicMethod("invoke_ParseJson", typeof(Object), new Type[] { typeof(string) }, true);
+                        var il = method.GetILGenerator();
 
-                // Store the value in a pseudo box until it's fully initialized
-                var boxType = typeof(PseudoBox<>).MakeGenericType(type);
+                        il.Emit(OpCodes.Ldarg_0);
+                        il.Emit(OpCodes.Call, parseJson);
+                        il.Emit(OpCodes.Box, type);
+                        il.Emit(OpCodes.Ret);
+                        var invoke = (Func<string, object>)method.CreateDelegate(typeof(Func<string, object>));
 
-                // Process all members
-                foreach (var m in ri.Members)
-                {
-                    // Ignore write only properties
-                    var pi = m.Member as PropertyInfo;
-                    var fi = m.Member as FieldInfo;
-                    if (pi != null && pi.GetSetMethod() == null)
-                    {
-                        continue;
+                        return (r, t) =>
+                        {
+                            if (r.GetLiteralKind() == LiteralKind.String)
+                            {
+                                var o = invoke(r.GetLiteralString());
+                                r.NextToken();
+                                return o;
+                            }
+                            throw new InvalidDataException(string.Format("Expected string literal for type {0}", type.FullName));
+                        };
                     }
+                }
+                else
+                {
+                    // Get the reflection info for this type
+                    var ri = ReflectionInfo.GetReflectionInfo(type);
+                    if (ri == null)
+                        return null;
 
-                    // Create a dynamic method that can do the work
-                    var method = new DynamicMethod("dynamic_parser", null, new Type[] { typeof(IJsonReader), typeof(object) }, true);
-                    var il = method.GetILGenerator();
+                    // We'll create setters for each property/field
+                    var setters = new Dictionary<string, Action<IJsonReader, object>>();
 
-                    // Load the target
-                    il.Emit(OpCodes.Ldarg_1);
-                    il.Emit(OpCodes.Castclass, boxType);
-                    il.Emit(OpCodes.Ldflda, boxType.GetField("value"));
+                    // Store the value in a pseudo box until it's fully initialized
+                    var boxType = typeof(PseudoBox<>).MakeGenericType(type);
 
-                    // Get the value
-                    GenerateGetJsonValue(m, il);
+                    // Process all members
+                    foreach (var m in ri.Members)
+                    {
+                        // Ignore write only properties
+                        var pi = m.Member as PropertyInfo;
+                        var fi = m.Member as FieldInfo;
+                        if (pi != null && pi.GetSetMethod() == null)
+                        {
+                            continue;
+                        }
 
-                    // Assign it
-                    if (pi != null)
-                        il.Emit(OpCodes.Call, pi.GetSetMethod());
-                    if (fi != null)
-                        il.Emit(OpCodes.Stfld, fi);
+                        // Create a dynamic method that can do the work
+                        var method = new DynamicMethod("dynamic_parser", null, new Type[] { typeof(IJsonReader), typeof(object) }, true);
+                        var il = method.GetILGenerator();
 
-                    // Done
-                    il.Emit(OpCodes.Ret);
+                        // Load the target
+                        il.Emit(OpCodes.Ldarg_1);
+                        il.Emit(OpCodes.Castclass, boxType);
+                        il.Emit(OpCodes.Ldflda, boxType.GetField("value"));
 
-                    // Store in the map of setters
-                    setters.Add(m.JsonKey, (Action<IJsonReader, object>)method.CreateDelegate(typeof(Action<IJsonReader, object>)));
-                }
+                        // Get the value
+                        GenerateGetJsonValue(m, il);
 
-                // Create helpers to invoke the interfaces (this is painful but avoids having to really box 
-                // the value in order to call the interface).
-                Action<object, IJsonReader> invokeLoading = MakeInterfaceCall(type, typeof(IJsonLoading));
-                Action<object, IJsonReader> invokeLoaded = MakeInterfaceCall(type, typeof(IJsonLoaded));
-                Func<object, IJsonReader, string, bool> invokeField = MakeLoadFieldCall(type);
+                        // Assign it
+                        if (pi != null)
+                            il.Emit(OpCodes.Call, pi.GetSetMethod());
+                        if (fi != null)
+                            il.Emit(OpCodes.Stfld, fi);
 
-                // Create the parser
-                Func<IJsonReader, Type, object> parser = (reader, Type) =>
-                {
-                    // Create pseudobox (ie: new PseudoBox<Type>)
-                    var box = Activator.CreateInstance(boxType);
+                        // Done
+                        il.Emit(OpCodes.Ret);
 
-                    // Call IJsonLoading
-                    if (invokeLoading != null)
-                        invokeLoading(box, reader);
+                        // Store in the map of setters
+                        setters.Add(m.JsonKey, (Action<IJsonReader, object>)method.CreateDelegate(typeof(Action<IJsonReader, object>)));
+                    }
 
-                    // Read the dictionary
-                    reader.ParseDictionary(key =>
+                    // Create helpers to invoke the interfaces (this is painful but avoids having to really box 
+                    // the value in order to call the interface).
+                    Action<object, IJsonReader> invokeLoading = MakeInterfaceCall(type, typeof(IJsonLoading));
+                    Action<object, IJsonReader> invokeLoaded = MakeInterfaceCall(type, typeof(IJsonLoaded));
+                    Func<object, IJsonReader, string, bool> invokeField = MakeLoadFieldCall(type);
+
+                    // Create the parser
+                    Func<IJsonReader, Type, object> parser = (reader, Type) =>
                     {
-                        // Call IJsonLoadField
-                        if (invokeField != null && invokeField(box, reader, key))
-                            return;
+                        // Create pseudobox (ie: new PseudoBox<Type>)
+                        var box = Activator.CreateInstance(boxType);
 
-                        // Get a setter and invoke it if found
-                        Action<IJsonReader, object> setter;
-                        if (setters.TryGetValue(key, out setter))
+                        // Call IJsonLoading
+                        if (invokeLoading != null)
+                            invokeLoading(box, reader);
+
+                        // Read the dictionary
+                        reader.ParseDictionary(key =>
                         {
-                            setter(reader, box);
-                        }
-                    });
+                            // Call IJsonLoadField
+                            if (invokeField != null && invokeField(box, reader, key))
+                                return;
 
-                    // IJsonLoaded
-                    if (invokeLoaded != null)
-                        invokeLoaded(box, reader);
+                            // Get a setter and invoke it if found
+                            Action<IJsonReader, object> setter;
+                            if (setters.TryGetValue(key, out setter))
+                            {
+                                setter(reader, box);
+                            }
+                        });
 
-                    // Return the value
-                    return ((IPseudoBox)box).GetValue();
-                };
+                        // IJsonLoaded
+                        if (invokeLoaded != null)
+                            invokeLoaded(box, reader);
 
-                // Done
-                return parser;
+                        // Return the value
+                        return ((IPseudoBox)box).GetValue();
+                    };
+
+                    // Done
+                    return parser;
+                }
             }
 
             // Helper to make the call to a PsuedoBox value's IJsonLoading or IJsonLoaded

+ 69 - 23
TestCases/TestCustomFormat.cs

@@ -10,59 +10,105 @@ using System.Reflection;
 
 namespace TestCases
 {
-    struct Point
+    struct PointSimple
     {
         public int X;
         public int Y;
+
+        private string FormatJson()
+        {
+            return string.Format("{0},{1}", X, Y);
+        }
+
+        private static PointSimple ParseJson(string literal)
+        {
+            var parts = literal.Split(',');
+            if (parts.Length == 2)
+            {
+                return new PointSimple()
+                {
+                    X = int.Parse(parts[0], CultureInfo.InvariantCulture),
+                    Y = int.Parse(parts[0], CultureInfo.InvariantCulture),
+                };
+            }
+            throw new InvalidDataException("Invalid point");
+        }
+    }
+    struct PointComplex
+    {
+        public int X;
+        public int Y;
+
+        private void FormatJson(IJsonWriter writer)
+        {
+            writer.WriteStringLiteral(string.Format("{0},{1}", X, Y));
+        }
+
+        private static PointComplex ParseJson(IJsonReader r)
+        {
+            if (r.GetLiteralKind() == LiteralKind.String)
+            {
+                var parts = ((string)r.GetLiteralString()).Split(',');
+                if (parts.Length == 2)
+                {
+                    var pt = new PointComplex()
+                    {
+                        X = int.Parse(parts[0], CultureInfo.InvariantCulture),
+                        Y = int.Parse(parts[0], CultureInfo.InvariantCulture),
+                    };
+                    r.NextToken();
+                    return pt;
+                }
+            }
+            throw new InvalidDataException("Invalid point");
+        }
+
     }
 
     [TestFixture]
     [Obfuscation(Exclude = true, ApplyToMembers = true)]
     public class TestCustomFormat
     {
-        static TestCustomFormat()
+        [Test]
+        public void TestSimple()
         {
-            // Custom formatter
-            Json.RegisterFormatter<Point>( (writer,point) => 
-            {
-                writer.WriteStringLiteral(string.Format("{0},{1}", point.X, point.Y));
-            });
+            var p = new PointSimple() { X = 10, Y = 20 };
 
-            // Custom parser
-            Json.RegisterParser<Point>( literal => {
+            var json = Json.Format(p);
 
-                var parts = ((string)literal).Split(',');
-                if (parts.Length!=2)
-                    throw new InvalidDataException("Badly formatted point");
+            Assert.AreEqual(json, "\"10,20\"");
 
-                return new Point()
-                {
-                    X = int.Parse(parts[0], CultureInfo.InvariantCulture),
-                    Y = int.Parse(parts[0], CultureInfo.InvariantCulture),
-                };
+            var p2 = Json.Parse<PointSimple>(json);
 
-            });
+            Assert.Equals(p.X, p2.X);
+            Assert.Equals(p.Y, p2.Y);
+        }
+
+        [Test]
+        public void TestSimpleExceptionPassed()
+        {
+            Assert.Throws<JsonParseException>(() => Json.Parse<PointSimple>("\"10,20,30\""));
         }
 
         [Test]
-        public void Test()
+        public void TestComplex()
         {
-            var p = new Point() { X = 10, Y = 20 };
+            var p = new PointComplex() { X = 10, Y = 20 };
 
             var json = Json.Format(p);
 
             Assert.AreEqual(json, "\"10,20\"");
 
-            var p2 = Json.Parse<Point>(json);
+            var p2 = Json.Parse<PointComplex>(json);
 
             Assert.Equals(p.X, p2.X);
             Assert.Equals(p.Y, p2.Y);
         }
 
         [Test]
-        public void TestExceptionPassed()
+        public void TestComplexExceptionPassed()
         {
-            Assert.Throws<JsonParseException>(() => Json.Parse<Point>("\"10,20,30\""));
+            Assert.Throws<JsonParseException>(() => Json.Parse<PointComplex>("\"10,20,30\""));
         }
     }
 }

+ 46 - 1
readme.md

@@ -206,7 +206,7 @@ and we want to serialize points as a comma separated string like this:
 		"BottomRight: "30,40",
 	}
 
-To do this, we need to register a formatter:
+To do this, we can to register a formatter:
 
     // Register custom formatter
     Json.RegisterFormatter<Point>( (writer,point) => 
@@ -241,6 +241,51 @@ We can now format and parse Points:
 Note that in this example we're formatting to a single string literal.  We can do more
 complex custom serialization using the IJsonReader and IJsonWriter interfaces - see below.
 
+## Simple Formatting for Value Types
+
+The problem with the above methods is that it requires pre-registering the formatters and 
+parsers which can be a pain. Another way to do custom formatting for value types is by 
+implementing methods directly on the struct.  This approach is more intrusive but also 
+more self-contained.
+
+The methods must have the following method names and signatures and can be public or not.
+
+	// One of these:
+	void FormatJson(IJsonWriter w);
+	string FormatJson();
+
+	// And one of these
+	static T ParseJson(IJsonReader r);
+	static T ParseJson(string literal);
+
+For example, this is the equivalent of the above example:
+
+    struct Point
+    {
+        public int X;
+        public int Y;
+
+		string FormatJson() 
+		{ 
+			return string.Format("{0},{1}", X, Y);
+		};
+
+		static Point ParseJson(string literal)
+		{
+			var parts = literal.Split(',');
+			if (parts.Length!=2)
+				throw new InvalidDataException("Badly formatted point");
+
+			return new Point()
+			{
+				X = int.Parse(parts[0], CultureInfo.InvariantCulture),
+				Y = int.Parse(parts[0], CultureInfo.InvariantCulture),
+			};
+		}
+    }
+
+Note: this approach only works for structs (not classes)
+
 ## Custom Factories
 
 Suppose we have a class heirarchy something like this: