Browse Source

XML docs and added support for ExcludeIfNull

Brad Robinson 3 năm trước cách đây
mục cha
commit
783634f35a

+ 71 - 0
Topten.JsonKit.Test/TestExcludeIfNull.cs

@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Topten.JsonKit;
+using System.IO;
+using System.Reflection;
+using Xunit;
+
+namespace TestCases
+{
+    [Obfuscation(Exclude = true, ApplyToMembers = true)]
+    public class TestExcludeIfNull
+    {
+        class Thing
+        {
+            [Json("field", ExcludeIfNull = true)]
+            public string Field;
+
+            [Json("property", ExcludeIfNull = true)]
+            public string Property { get; set; }
+
+            [Json("nfield", ExcludeIfNull = true)]
+            public int? NField;
+
+            [Json("nproperty", ExcludeIfNull = true)]
+            public int? NProperty { get; set; }
+
+        }
+
+        [Fact]
+        public void TestDoesntWriteNull()
+        {
+            var thing = new Thing();
+
+            // Save it
+            var json = Json.Format(thing);
+
+            // Check the object kinds were written out
+            Assert.DoesNotContain("\"field\":", json);
+            Assert.DoesNotContain("\"property\":", json);
+            Assert.DoesNotContain("\"nfield\":", json);
+            Assert.DoesNotContain("\"nproperty\":", json);
+        }
+
+        [Fact]
+        public void TestDoesWriteNonNull()
+        {
+            var thing = new Thing()
+            {
+                Field = "blah",
+                Property = "deblah",
+                NField = 23,
+                NProperty = 24,
+            };
+
+            // Save it
+            var json = Json.Format(thing);
+
+            // Check the object kinds were written out
+            Assert.Contains("\"field\":", json);
+            Assert.Contains("\"property\":", json);
+            Assert.Contains("\"nfield\":", json);
+            Assert.Contains("\"nproperty\":", json);
+            Assert.Contains("\"blah\"", json);
+            Assert.Contains("\"deblah\"", json);
+            Assert.Contains("23", json);
+            Assert.Contains("24", json);
+        }
+    }
+}

+ 31 - 0
Topten.JsonKit.sln

@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.31105.61
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Topten.JsonKit", "Topten.JsonKit\Topten.JsonKit.csproj", "{6A4B6010-62C1-4DC8-971D-C0BAD62CC65D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Topten.JsonKit.Test", "Topten.JsonKit.Test\Topten.JsonKit.Test.csproj", "{4813BF7A-D2CF-46D9-945D-5A17C609E8D2}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{6A4B6010-62C1-4DC8-971D-C0BAD62CC65D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{6A4B6010-62C1-4DC8-971D-C0BAD62CC65D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{6A4B6010-62C1-4DC8-971D-C0BAD62CC65D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{6A4B6010-62C1-4DC8-971D-C0BAD62CC65D}.Release|Any CPU.Build.0 = Release|Any CPU
+		{4813BF7A-D2CF-46D9-945D-5A17C609E8D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{4813BF7A-D2CF-46D9-945D-5A17C609E8D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{4813BF7A-D2CF-46D9-945D-5A17C609E8D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{4813BF7A-D2CF-46D9-945D-5A17C609E8D2}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {23B58C55-12CD-4C23-B625-E36393CBF8A7}
+	EndGlobalSection
+EndGlobal

+ 1 - 1
Topten.JsonKit/DecoratingActivator.cs

@@ -19,7 +19,7 @@ using System;
 namespace Topten.JsonKit
 {
     // Helper to create instances but include the type name in the thrown exception
-    public static class DecoratingActivator
+    static class DecoratingActivator
     {
         public static object CreateInstance(Type t)
         {

+ 57 - 27
Topten.JsonKit/Emit.cs

@@ -22,14 +22,12 @@ using System.Reflection;
 using System.Globalization;
 using System.Reflection.Emit;
 
-
-
 namespace Topten.JsonKit
 {
     static class Emit
     {
 
-        // Generates a function that when passed an object of specified type, renders it to an IJsonReader
+        // Generates a function that when passed an object of specified type, renders it to an IJsonWriter
         public static Action<IJsonWriter, object> MakeFormatter(Type type)
         {
             var formatJson = ReflectionInfo.FindFormatJson(type);
@@ -81,15 +79,15 @@ namespace Topten.JsonKit
 
                 // 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)
-            };
+                    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[] {
-                typeof(double), typeof(float), typeof(string), typeof(char)
-            };
+                    typeof(double), typeof(float), typeof(string), typeof(char)
+                };
 
                 // Call IJsonWriting if implemented
                 if (typeof(IJsonWriting).IsAssignableFrom(type))
@@ -125,11 +123,6 @@ namespace Topten.JsonKit
                         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);
 
@@ -185,31 +178,49 @@ namespace Topten.JsonKit
                         }
                     }
 
-                    Label? lblFinished = null;
+                    Label lblFinished = il.DefineLabel();
+
+                    void EmitWriteKey()
+                    {
+                        // 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) }));
+                    }
 
                     // 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();
 
-                        // Call has_Value
+                        // Call HasValue
+                        il.Emit(OpCodes.Dup);
                         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);
+                        // HasValue returned false, so either omit the key entirely, or write it as "null"
+                        if (m.ExcludeIfNull)
+                        {
+                            il.Emit(OpCodes.Pop);       // Pop value
+                            il.Emit(OpCodes.Pop);       // Pop writer
+                        }
+                        else
+                        {
+                            // Write the key
+                            EmitWriteKey();
+
+                            // 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);
 
                         // Get it's value
                         il.MarkLabel(lblHasValue);
+                        EmitWriteKey();
                         il.Emit(OpCodes.Call, memberType.GetProperty("Value").GetGetMethod());
 
                         // Switch to the underlying type from here on
@@ -224,6 +235,26 @@ namespace Topten.JsonKit
                             il.Emit(OpCodes.Ldloca, locTemp);
                         }
                     }
+                    else
+                    {
+                        if (m.ExcludeIfNull && !type.IsValueType)
+                        {
+                            var lblContinue = il.DefineLabel();
+                            System.Diagnostics.Debug.Assert(!NeedValueAddress);
+                            il.Emit(OpCodes.Dup);
+                            il.Emit(OpCodes.Brtrue_S, lblContinue);
+
+                            il.Emit(OpCodes.Pop);           // Pop value
+                            il.Emit(OpCodes.Pop);           // Pop writer
+
+                            // Define some labels
+                            il.Emit(OpCodes.Br_S, lblFinished);
+
+                            il.MarkLabel(lblContinue);
+                        }
+
+                        EmitWriteKey();
+                    }
 
                     // ToString()
                     if (toStringTypes.Contains(memberType))
@@ -282,8 +313,7 @@ namespace Topten.JsonKit
                         il.Emit(OpCodes.Callvirt, typeof(IJsonWriter).GetMethod("WriteValue", new Type[] { typeof(object) }));
                     }
 
-                    if (lblFinished.HasValue)
-                        il.MarkLabel(lblFinished.Value);
+                    il.MarkLabel(lblFinished);
                 }
 
                 // Call IJsonWritten

+ 52 - 2
Topten.JsonKit/IDictionaryExtensions.cs

@@ -6,9 +6,25 @@ using System.Threading.Tasks;
 
 namespace Topten.JsonKit
 {
+    /// <summary>
+    /// Helper extensions for navigating dictionaries of string/object
+    /// </summary>
     public static class IDictionaryExtensions
     {
-        public static bool WalkPath(this IDictionary<string, object> This, string Path, bool create, Func<IDictionary<string,object>,string, bool> leafCallback)
+        /// <summary>
+        /// Navigates a string/object dictionary following a path
+        /// </summary>
+        /// <param name="This">The root dictionary</param>
+        /// <param name="Path">The path to follow</param>
+        /// <param name="create">Whether to create nodes as walking the path</param>
+        /// <param name="leafCallback">A callback invoked for each lead node.  Return false to stop walk</param>
+        /// <returns>Result of last leafCallback, or false if key not found and create parameter is false</returns>
+        public static bool WalkPath(
+            this IDictionary<string, object> This, 
+            string Path, 
+            bool create, 
+            Func<IDictionary<string,object>,string, bool> leafCallback
+            )
         {
             // Walk the path
             var parts = Path.Split('.');
@@ -30,11 +46,25 @@ namespace Topten.JsonKit
             return leafCallback(This, parts[parts.Length-1]);
         }
 
+        /// <summary>
+        /// Check if a path exists in an string/object dictionary heirarchy
+        /// </summary>
+        /// <param name="This">The root dictionary</param>
+        /// <param name="Path">The path to follow</param>
+        /// <returns>True if the path exists</returns>
         public static bool PathExists(this IDictionary<string, object> This, string Path)
         {
             return This.WalkPath(Path, false, (dict, key) => dict.ContainsKey(key));
         }
 
+        /// <summary>
+        /// Gets the object at the specified path in an string/object dictionary heirarchy
+        /// </summary>
+        /// <param name="This">The root dictionary</param>
+        /// <param name="type">The expected returned object type</param>
+        /// <param name="Path">The path to follow</param>
+        /// <param name="def">The default value if the value isn't found</param>
+        /// <returns></returns>
         public static object GetPath(this IDictionary<string, object> This, Type type, string Path, object def)
         {
             This.WalkPath(Path, false, (dict, key) =>
@@ -55,7 +85,13 @@ namespace Topten.JsonKit
             return def;
         }
 
-        // Ensure there's an object of type T at specified path
+        /// <summary>
+        /// Gets an object of specified type at a path location in a string/object dictionaryt
+        /// </summary>
+        /// <typeparam name="T">The returned object type</typeparam>
+        /// <param name="This">The root dictionary</param>
+        /// <param name="Path">The path of the object to return</param>
+        /// <returns>The object instance</returns>
         public static T GetObjectAtPath<T>(this IDictionary<string, object> This, string Path) where T:class,new()
         {
             T retVal = null;
@@ -75,11 +111,25 @@ namespace Topten.JsonKit
             return retVal;
         }
 
+        /// <summary>
+        /// Get a value at a path in a string/object dictionary heirarchy
+        /// </summary>
+        /// <typeparam name="T">The type of object to be returned</typeparam>
+        /// <param name="This">The root dictionary</param>
+        /// <param name="Path">The path of the entry to find</param>
+        /// <param name="def">The default value to return if not found</param>
+        /// <returns>The located value, or the default value if not found</returns>
         public static T GetPath<T>(this IDictionary<string, object> This, string Path, T def = default(T))
         {
             return (T)This.GetPath(typeof(T), Path, def);
         }
 
+        /// <summary>
+        /// Set a value in a string/object dictionary heirarchy
+        /// </summary>
+        /// <param name="This">The root dictionary</param>
+        /// <param name="Path">The path of the value to set</param>
+        /// <param name="value">The value to set</param>
         public static void SetPath(this IDictionary<string, object> This, string Path, object value)
         {
             This.WalkPath(Path, true, (dict, key) => { dict[key] = value; return true; });

+ 10 - 2
Topten.JsonKit/IJsonLoadField.cs

@@ -17,11 +17,19 @@ using System.Reflection;
 
 namespace Topten.JsonKit
 {
-    // Called for each field while loading from reflection
-    // Return true if handled
+    /// <summary>
+    /// Optional interface which if implemented on objects serialized will be called after
+    /// called for each field while loading.
+    /// </summary>
     [Obfuscation(Exclude = true, ApplyToMembers = true)]
     public interface IJsonLoadField
     {
+        /// <summary>
+        /// Notifies the object that the field is being loaded
+        /// </summary>
+        /// <param name="r">The reader loading the JSON</param>
+        /// <param name="key">The key being loaded</param>
+        /// <returns>True if the field has been handled and doesn't need to be assigned by the loader</returns>
         bool OnJsonField(IJsonReader r, string key);
     }
 }

+ 8 - 1
Topten.JsonKit/IJsonLoaded.cs

@@ -18,10 +18,17 @@ using System.Reflection;
 
 namespace Topten.JsonKit
 {
-    // Called after loading via reflection
+    /// <summary>
+    /// Optional interface which if implemented on objects serialized will be called after
+    /// the object has been loaded
+    /// </summary>
     [Obfuscation(Exclude = true, ApplyToMembers = true)]
     public interface IJsonLoaded
     {
+        /// <summary>
+        /// Notifies the object that it's been loaded via JSON
+        /// </summary>
+        /// <param name="r">The reader that loaded the object</param>
         void OnJsonLoaded(IJsonReader r);
     }
 }

+ 8 - 1
Topten.JsonKit/IJsonLoading.cs

@@ -17,10 +17,17 @@ using System.Reflection;
 
 namespace Topten.JsonKit
 {
-    // Called before loading via reflection
+    /// <summary>
+    /// Optional interface which if implemented on objects serialized will be called just before
+    /// the object is loaded.
+    /// </summary>
     [Obfuscation(Exclude = true, ApplyToMembers = true)]
     public interface IJsonLoading
     {
+        /// <summary>
+        /// Notifies the object that it's about to be loaded via JSON
+        /// </summary>
+        /// <param name="r">The reader that will load the object</param>
         void OnJsonLoading(IJsonReader r);
     }
 }

+ 51 - 1
Topten.JsonKit/IJsonReader.cs

@@ -18,21 +18,71 @@ using System.Reflection;
 
 namespace Topten.JsonKit
 {
-    // Passed to registered parsers
+    /// <summary>
+    /// Provides a reader for JSON data
+    /// </summary>
     [Obfuscation(Exclude=true, ApplyToMembers=true)]
     public interface IJsonReader
     {
+        /// <summary>
+        /// Parses an object of specified type from the JSON stream
+        /// </summary>
+        /// <param name="type">The type to be parsed</param>
+        /// <returns>A reference to the loaded instance</returns>
         object Parse(Type type);
+
+        /// <summary>
+        /// Parses an object of specified type from the JSON stream
+        /// </summary>
+        /// <typeparam name="T">The type to be parsed</typeparam>
+        /// <returns>A reference to the loaded instance</returns>
         T Parse<T>();
+
+        /// <summary>
+        /// Parses from a JSON stream into an existing object instance
+        /// </summary>
+        /// <param name="into">The target object</param>
         void ParseInto(object into);
 
+        /// <summary>
+        /// The current token in the input JSON stream
+        /// </summary>
         Token CurrentToken { get; }
+
+        /// <summary>
+        /// Reads a literal value from the JSON stream
+        /// </summary>
+        /// <param name="converter">A converter function to convert the value</param>
+        /// <returns>The parsed and converted value</returns>
         object ReadLiteral(Func<object, object> converter);
+
+        /// <summary>
+        /// Reads a dictinary from the input stream
+        /// </summary>
+        /// <param name="callback">A callback that will be invoked for each encountered dictionary key</param>
         void ParseDictionary(Action<string> callback);
+
+        /// <summary>
+        /// Reads an array from the input stream
+        /// </summary>
+        /// <param name="callback">A callback that will be invoked as each array element is encounters</param>
         void ParseArray(Action callback);
 
+        /// <summary>
+        /// Gets the literal kind of the current stream token
+        /// </summary>
+        /// <returns></returns>
         LiteralKind GetLiteralKind();
+
+        /// <summary>
+        /// Gets a string literal from the JSON stream
+        /// </summary>
+        /// <returns></returns>
         string GetLiteralString();
+
+        /// <summary>
+        /// Moves to the next token in the input stream
+        /// </summary>
         void NextToken();
     }
 }

+ 39 - 1
Topten.JsonKit/IJsonWriter.cs

@@ -18,17 +18,55 @@ using System.Reflection;
 
 namespace Topten.JsonKit
 {
-    // Passed to registered formatters
+    /// <summary>
+    /// Writes to a JSON output stream
+    /// </summary>
     [Obfuscation(Exclude = true, ApplyToMembers = true)]
     public interface IJsonWriter
     {
+        /// <summary>
+        /// Writes a string literal
+        /// </summary>
+        /// <param name="str">The string to write</param>
         void WriteStringLiteral(string str);
+
+        /// <summary>
+        /// Write raw characters to the output stream
+        /// </summary>
+        /// <param name="str">The string to write</param>
         void WriteRaw(string str);
+
+        /// <summary>
+        /// Writes array delimeters to the output stream
+        /// </summary>
+        /// <param name="callback">A callback that should write the array elements</param>
         void WriteArray(Action callback);
+
+        /// <summary>
+        /// Writes dictionary delimeters to the output stream
+        /// </summary>
+        /// <param name="callback">A callback that should write the dictionary keys and values</param>
         void WriteDictionary(Action callback);
+
+        /// <summary>
+        /// Writes a value to the output stream
+        /// </summary>
+        /// <param name="value">The value to write</param>
         void WriteValue(object value);
+
+        /// <summary>
+        /// Writes the separator between array elements
+        /// </summary>
         void WriteElement();
+
+        /// <summary>
+        /// Writes a dictionary key to the output stream
+        /// </summary>
         void WriteKey(string key);
+
+        /// <summary>
+        /// Writes a dictionary key to the output stream without escaping the key
+        /// </summary>
         void WriteKeyNoEscaping(string key);
     }
 }

+ 8 - 1
Topten.JsonKit/IJsonWriting.cs

@@ -17,10 +17,17 @@ using System.Reflection;
 
 namespace Topten.JsonKit
 {
-    // Called when about to write using reflection
+    /// <summary>
+    /// Optional interface which if implemented on objects serialized will be called just before
+    /// the object is saved.
+    /// </summary>
     [Obfuscation(Exclude = true, ApplyToMembers = true)]
     public interface IJsonWriting
     {
+        /// <summary>
+        /// Notifies the object that it's about to be written
+        /// </summary>
+        /// <param name="w">The writer the object will be written to</param>
         void OnJsonWriting(IJsonWriter w);
     }
 }

+ 8 - 1
Topten.JsonKit/IJsonWritten.cs

@@ -17,10 +17,17 @@ using System.Reflection;
 
 namespace Topten.JsonKit
 {
-    // Called after written using reflection
+    /// <summary>
+    /// Optional interface which if implemented on objects serialized will be called after 
+    /// the object is saved.
+    /// </summary>
     [Obfuscation(Exclude = true, ApplyToMembers = true)]
     public interface IJsonWritten
     {
+        /// <summary>
+        /// Notifies the object that it's been written
+        /// </summary>
+        /// <param name="w">The writer the object wrote the object</param>
         void OnJsonWritten(IJsonWriter w);
     }
 }

+ 72 - 28
Topten.JsonKit/JsonAttribute.cs

@@ -16,59 +16,103 @@ using System;
 
 namespace Topten.JsonKit
 {
-    // Used to decorate fields and properties that should be serialized
-    //
-    // - [Json] on class or struct causes all public fields and properties to be serialized
-    // - [Json] on a public or non-public field or property causes that member to be serialized
-    // - [JsonExclude] on a field or property causes that field to be not serialized
-    // - A class or struct with no [Json] attribute has all public fields/properties serialized
-    // - A class or struct with no [Json] attribute but a [Json] attribute on one or more members only serializes those members
-    //
-    // Use [Json("keyname")] to explicitly specify the key to be used 
-    // [Json] without the keyname will be serialized using the name of the member with the first letter lowercased.
-    //
-    // [Json(KeepInstance=true)] causes container/subobject types to be serialized into the existing member instance (if not null)
-    //
-    // You can also use the system supplied DataContract and DataMember attributes.  They'll only be used if there
-    // are no JsonKit attributes on the class or it's members. You must specify DataContract on the type and
-    // DataMember on any fields/properties that require serialization.  There's no need for exclude attribute.
-    // When using DataMember, the name of the field or property is used as is - the first letter is left in upper case
-    //
+    /// <summary>
+    /// Used to decorate fields and properties that should be serialized
+    /// </summary>
+    /// <remarks>
+    /// - [Json] on class or struct causes all public fields and properties to be serialized
+    /// - [Json] on a public or non-public field or property causes that member to be serialized
+    /// - [JsonExclude] on a field or property causes that field to be not serialized
+    /// - A class or struct with no [Json] attribute has all public fields/properties serialized
+    /// - A class or struct with no [Json] attribute but a [Json] attribute on one or more members only serializes those members
+    ///
+    /// Use [Json("keyname")] to explicitly specify the key to be used 
+    /// [Json] without the keyname will be serialized using the name of the member with the first letter lowercased.
+    ///
+    /// [Json(KeepInstance=true)] causes container/subobject types to be serialized into the existing member instance (if not null)
+    ///
+    /// You can also use the system supplied DataContract and DataMember attributes.  They'll only be used if there
+    /// are no JsonKit attributes on the class or it's members. You must specify DataContract on the type and
+    /// DataMember on any fields/properties that require serialization.  There's no need for exclude attribute.
+    /// When using DataMember, the name of the field or property is used as is - the first letter is left in upper case
+    /// </remarks>
     [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property | AttributeTargets.Field)]
     public class JsonAttribute : Attribute
     {
+        /// <summary>
+        /// Constructs a new Json attribute
+        /// </summary>
         public JsonAttribute()
         {
-            _key = null;
+            Key = null;
         }
 
+        /// <summary>
+        /// Constructs a new Json attribute, setting the key used for serialization of this item
+        /// </summary>
+        /// <param name="key"></param>
         public JsonAttribute(string key)
         {
-            _key = key;
+            Key = key;
         }
 
-        // Key used to save this field/property
-        string _key;
+
+        /// <summary>
+        /// Gets the serialization key for this Json item
+        /// </summary>
         public string Key
         {
-            get { return _key; }
+            get;
+            private set;
         }
 
-        // If true uses ParseInto to parse into the existing object instance
-        // If false, creates a new instance as assigns it to the property
+        /// <summary>
+        /// Controls whether a collection is serialized by creating and assigning a new
+        /// instance, or by clearing and serializing into the existing instance.
+        /// </summary>
+        /// <remarks>
+        /// If true uses ParseInto to parse into the existing object instance
+        /// If false, creates a new instance as assigns it to the property
+        /// </remarks>
         public bool KeepInstance
         {
             get;
             set;
         }
 
-        // If true, the property will be loaded, but not saved
-        // Use to upgrade deprecated persisted settings, but not
-        // write them back out again
+        /// <summary>
+        /// Used to decorate deprecated properties that should be loaded
+        /// but not saved.
+        /// </summary>
+        /// <remarks>
+        /// If true, the property will be loaded, but not saved
+        /// Use to upgrade deprecated persisted settings, but not
+        /// write them back out again
+        /// </remarks>
         public bool Deprecated
         {
             get;
             set;
         }
+
+        /// <summary>
+        /// For non-value types controls whether null values should be
+        /// written as null, or excluded for output.
+        /// </summary>
+        public bool ExcludeIfNull
+        {
+            get;
+            set;
+        }
+
+        /// <summary>
+        /// For collection types controls whether they should be serialized
+        /// if the collection is empty.
+        /// </summary>
+        public bool ExcludeIfEmpty
+        {
+            get;
+            set;
+        }
     }
 }

+ 6 - 1
Topten.JsonKit/JsonExcludeAttribute.cs

@@ -16,10 +16,15 @@ using System;
 
 namespace Topten.JsonKit
 {
-    // See comments for JsonAttribute above
+    /// <summary>
+    /// Indicates that the target element should be excluded from serialization
+    /// </summary>
     [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
     public class JsonExcludeAttribute : Attribute
     {
+        /// <summary>
+        /// Constructs a new JsonExclude attribute
+        /// </summary>
         public JsonExcludeAttribute()
         {
         }

+ 194 - 37
Topten.JsonKit/JsonKit.cs

@@ -17,7 +17,9 @@ using System.IO;
 
 namespace Topten.JsonKit
 {
-    // API
+    /// <summary>
+    /// The main static interface class to JsonKit
+    /// </summary>
     public static class Json
     {
         static Json()
@@ -32,14 +34,18 @@ namespace Topten.JsonKit
 #endif
         }
 
-        // Pretty format default
+        /// <summary>
+        /// Controls whether the write whitespace by default
+        /// </summary>
         public static bool WriteWhitespaceDefault
         {
             get;
             set;
         }
 
-        // Strict parser
+        /// <summary>
+        /// Controls whether parsing should be strict by default
+        /// </summary>
         public static bool StrictParserDefault
         {
             get;
@@ -47,20 +53,36 @@ namespace Topten.JsonKit
         }
 
         // Write an object to a text writer
+        /// <summary>
+        /// Writes an object to a TextWriter
+        /// </summary>
+        /// <param name="w">The target text writer</param>
+        /// <param name="o">The object to be written</param>
+        /// <param name="options">Options controlling output formatting</param>
         public static void Write(TextWriter w, object o, JsonOptions options = JsonOptions.None)
         {
             var writer = new JsonWriter(w, ResolveOptions(options));
             writer.WriteValue(o);
         }
 
+
+        /// <summary>
+        /// Controls whether previous version should be saved if AutoSavePreviousVersion is used
+        /// </summary>
         public static bool SavePreviousVersions
         {
             get;
             set;
         }
 
-        // Write a file atomically by writing to a temp file and then renaming it - prevents corrupted files if crash 
-        // in middle of writing file.
+        /// <summary>
+        /// Write a file atomically by writing to a temp file and then renaming it - prevents corrupted files if crash 
+        /// in middle of writing file.
+        /// </summary>
+        /// <param name="filename">The output file name</param>
+        /// <param name="o">The object to be written</param>
+        /// <param name="options">Options controlling output</param>
+        /// <param name="backupFilename">An optional back filename where previous version will be written</param>
         public static void WriteFileAtomic(string filename, object o, JsonOptions options = JsonOptions.None, string backupFilename = null)
         {
             var tempName = filename + ".tmp";
@@ -132,7 +154,12 @@ namespace Topten.JsonKit
             }
         }
 
-        // Write an object to a file
+        /// <summary>
+        /// Write an object to a file
+        /// </summary>
+        /// <param name="filename">The output filename</param>
+        /// <param name="o">The object to be written</param>
+        /// <param name="options">Options controlling output</param>
         public static void WriteFile(string filename, object o, JsonOptions options = JsonOptions.None)
         {
             using (var w = new StreamWriter(filename))
@@ -147,7 +174,12 @@ namespace Topten.JsonKit
             }
         }
 
-        // Format an object as a json string
+        /// <summary>
+        /// Format an object as a json string
+        /// </summary>
+        /// <param name="o">The value to be formatted</param>
+        /// <param name="options">Options controlling output</param>
+        /// <returns>The formatted string</returns>
         public static string Format(object o, JsonOptions options = JsonOptions.None)
         {
             var sw = new StringWriter();
@@ -156,7 +188,13 @@ namespace Topten.JsonKit
             return sw.ToString();
         }
 
-        // Parse an object of specified type from a text reader
+        /// <summary>
+        /// Parse an object of specified type from a text reader 
+        /// </summary>
+        /// <param name="r">The text reader to read from</param>
+        /// <param name="type">The type of object to be parsed</param>
+        /// <param name="options">Options controlling parsing</param>
+        /// <returns>The parsed object</returns>
         public static object Parse(TextReader r, Type type, JsonOptions options = JsonOptions.None)
         {
             JsonReader reader = null;
@@ -175,13 +213,24 @@ namespace Topten.JsonKit
             }
         }
 
-        // Parse an object of specified type from a text reader
+        /// <summary>
+        /// Parse an object of specified type from a text reader 
+        /// </summary>
+        /// <typeparam name="T">The type of object to be parsed</typeparam>
+        /// <param name="r">The text reader to read from</param>
+        /// <param name="options">Options controlling parsing</param>
+        /// <returns>The parsed object</returns>
         public static T Parse<T>(TextReader r, JsonOptions options = JsonOptions.None)
         {
             return (T)Parse(r, typeof(T), options);
         }
 
-        // Parse from text reader into an already instantied object
+        /// <summary>
+        /// Parse from text reader into an already instantied object 
+        /// </summary>
+        /// <param name="r">The text reader to read from </param>
+        /// <param name="into">The object to serialize into</param>
+        /// <param name="options">Options controlling parsing</param>
         public static void ParseInto(TextReader r, Object into, JsonOptions options = JsonOptions.None)
         {
             if (into == null)
@@ -204,7 +253,13 @@ namespace Topten.JsonKit
             }
         }
 
-        // Parse an object of specified type from a file
+        /// <summary>
+        /// Parse an object of specified type from a file
+        /// </summary>
+        /// <param name="filename">The input filename</param>
+        /// <param name="type">The type of object to be parsed</param>
+        /// <param name="options">Options controlling parsing</param>
+        /// <returns>The parsed object instance</returns>
         public static object ParseFile(string filename, Type type, JsonOptions options = JsonOptions.None)
         {
             using (var r = new StreamReader(filename))
@@ -214,6 +269,13 @@ namespace Topten.JsonKit
         }
 
         // Parse an object of specified type from a file
+        /// <summary>
+        /// Parse an object of specified type from a file
+        /// </summary>
+        /// <typeparam name="T">The type of object to be parsed</typeparam>
+        /// <param name="filename">The input filename</param>
+        /// <param name="options">Options controlling parsing</param>
+        /// <returns></returns>
         public static T ParseFile<T>(string filename, JsonOptions options = JsonOptions.None)
         {
             using (var r = new StreamReader(filename))
@@ -222,7 +284,12 @@ namespace Topten.JsonKit
             }
         }
 
-        // Parse from file into an already instantied object
+        /// <summary>
+        /// Parse from file into an already instantied object 
+        /// </summary>
+        /// <param name="filename">The input filename</param>
+        /// <param name="into">The object to serialize into</param>
+        /// <param name="options">Options controlling parsing</param>
         public static void ParseFileInto(string filename, Object into, JsonOptions options = JsonOptions.None)
         {
             using (var r = new StreamReader(filename))
@@ -231,44 +298,80 @@ namespace Topten.JsonKit
             }
         }
 
-        // Parse an object from a string
+        /// <summary>
+        /// Parse an object from a string 
+        /// </summary>
+        /// <param name="data">The JSON data</param>
+        /// <param name="type">The type of object to be parsed</param>
+        /// <param name="options">Options controlling parsing</param>
+        /// <returns>The parsed object instance</returns>
         public static object Parse(string data, Type type, JsonOptions options = JsonOptions.None)
         {
             return Parse(new StringReader(data), type, options);
         }
 
-        // Parse an object from a string
+        /// <summary>
+        /// Parse an object from a string 
+        /// </summary>
+        /// <typeparam name="T">The type of object to be parsed</typeparam>
+        /// <param name="data">The JSON data</param>
+        /// <param name="options">Options controlling parsing</param>
+        /// <returns></returns>
         public static T Parse<T>(string data, JsonOptions options = JsonOptions.None)
         {
             return (T)Parse<T>(new StringReader(data), options);
         }
 
-        // Parse from string into an already instantiated object
+        /// <summary>
+        /// Parse from string into an already instantiated object 
+        /// </summary>
+        /// <param name="data">The JSON data</param>
+        /// <param name="into">The object to serialize into</param>
+        /// <param name="options">Options controlling parsing</param>
         public static void ParseInto(string data, Object into, JsonOptions options = JsonOptions.None)
         {
             ParseInto(new StringReader(data), into, options);
         }
 
-        // Create a clone of an object
+        /// <summary>
+        /// Create a clone of an object by serializing to JSON and the deserializing into a new instance
+        /// </summary>
+        /// <typeparam name="T">The type of object to be cloned</typeparam>
+        /// <param name="source">The object to be cloned</param>
+        /// <returns>A cloned instance</returns>
         public static T Clone<T>(T source)
         {
             return (T)Reparse(source.GetType(), source);
         }
 
         // Create a clone of an object (untyped)
+        /// <summary>
+        /// Create a clone of an object by serializing to JSON and the deserializing into a new instance
+        /// </summary>
+        /// <param name="source">The object to be cloned</param>
+        /// <returns>A cloned instance</returns>
         public static object Clone(object source)
         {
             return Reparse(source.GetType(), source);
         }
 
-        // Clone an object into another instance
+        /// <summary>
+        /// Clone an object into another instance 
+        /// </summary>
+        /// <param name="dest">The object to clone to</param>
+        /// <param name="source">The object to clone from</param>
         public static void CloneInto(object dest, object source)
         {
             ReparseInto(dest, source);
         }
 
-        // Reparse an object by writing to a stream and re-reading (possibly
-        // as a different type).
+        /// <summary>
+        /// Reparse an object by writing to a stream and re-reading (possibly
+        /// as a different type).
+        /// </summary>
+        /// <param name="type">The type of object to deserialize as</param>
+        /// <param name="source">The source object to be reparsed</param>
+        /// <returns>The newly parsed object instance</returns>
         public static object Reparse(Type type, object source)
         {
             if (source == null)
@@ -292,13 +395,23 @@ namespace Topten.JsonKit
             }
         }
 
-        // Typed version of above
+        /// <summary>
+        /// Reparse an object by writing to a stream and re-reading (possibly
+        /// as a different type).
+        /// </summary>
+        /// <typeparam name="T">The type of object to deserialize as</typeparam>
+        /// <param name="source">The source object to be reparsed</param>
+        /// <returns>The newly parsed object instance</returns>
         public static T Reparse<T>(object source)
         {
             return (T)Reparse(typeof(T), source);
         }
 
-        // Reparse one object into another object 
+        /// <summary>
+        /// Reparse one object into another object  
+        /// </summary>
+        /// <param name="dest">The destination object</param>
+        /// <param name="source">The source object</param>
         public static void ReparseInto(object dest, object source)
         {
             var ms = new MemoryStream();
@@ -320,82 +433,126 @@ namespace Topten.JsonKit
             }
         }
 
-        // Register a callback that can format a value of a particular type into json
+        /// <summary>
+        /// Register a callback that can format a value of a particular type into json 
+        /// </summary>
+        /// <param name="type">The type of object to be formatted</param>
+        /// <param name="formatter">The formatter callback</param>
         public static void RegisterFormatter(Type type, Action<IJsonWriter, object> formatter)
         {
             JsonWriter._formatters[type] = formatter;
         }
 
-        // Typed version of above
+        /// <summary>
+        /// Register a callback that can format a value of a particular type into json 
+        /// </summary>
+        /// <typeparam name="T">The type of object to be formatted</typeparam>
+        /// <param name="formatter">The formatter callback</param>
         public static void RegisterFormatter<T>(Action<IJsonWriter, T> formatter)
         {
             RegisterFormatter(typeof(T), (w, o) => formatter(w, (T)o));
         }
 
-        // Register a parser for a specified type
+        /// <summary>
+        /// Register a parser for a specified type
+        /// </summary>
+        /// <param name="type">The type of object to be parsed</param>
+        /// <param name="parser">The parser callback</param>
         public static void RegisterParser(Type type, Func<IJsonReader, Type, object> parser)
         {
             JsonReader._parsers.Set(type, parser);
         }
 
-        // Register a typed parser
+        /// <summary>
+        /// Register a parser for a specified type
+        /// </summary>
+        /// <typeparam name="T">The type of object to be parsed</typeparam>
+        /// <param name="parser">The parser callback</param>
         public static void RegisterParser<T>(Func<IJsonReader, Type, T> parser)
         {
             RegisterParser(typeof(T), (r, t) => parser(r, t));
         }
 
-        // Simpler version for simple types
+        /// <summary>
+        /// Registers a parser for a simple literal type
+        /// </summary>
+        /// <param name="type">The type to be parsed</param>
+        /// <param name="parser">The parser callback</param>
         public static void RegisterParser(Type type, Func<object, object> parser)
         {
             RegisterParser(type, (r, t) => r.ReadLiteral(parser));
         }
 
-        // Simpler and typesafe parser for simple types
+        /// <summary>
+        /// Register a parser for a simple literal type
+        /// </summary>
+        /// <typeparam name="T">The type to be parsed</typeparam>
+        /// <param name="parser">The parser callback</param>
         public static void RegisterParser<T>(Func<object, T> parser)
         {
             RegisterParser(typeof(T), literal => parser(literal));
         }
 
-        // Register an into parser
+        /// <summary>
+        /// Register a parser for loading into an existing instance
+        /// </summary>
+        /// <param name="type">The type to be parsed</param>
+        /// <param name="parser">The parser callback</param>
         public static void RegisterIntoParser(Type type, Action<IJsonReader, object> parser)
         {
             JsonReader._intoParsers.Set(type, parser);
         }
 
-        // Register an into parser
+
+        /// <summary>
+        /// Register a parser for loading into an existing instance
+        /// </summary>
+        /// <typeparam name="T">The type to be parsed</typeparam>
+        /// <param name="parser">The parser callback</param>
         public static void RegisterIntoParser<T>(Action<IJsonReader, object> parser)
         {
             RegisterIntoParser(typeof(T), parser);
         }
 
-        // Register a factory for instantiating objects (typically abstract classes)
-        // Callback will be invoked for each key in the dictionary until it returns an object
-        // instance and which point it will switch to serialization using reflection
+        /// <summary>
+        /// Register a factory for instantiating objects (typically abstract classes)
+        /// Callback will be invoked for each key in the dictionary until it returns an object
+        /// instance and which point it will switch to serialization using reflection
+        /// </summary>
+        /// <param name="type">The type to be instantiated</param>
+        /// <param name="factory">The factory callback</param>
         public static void RegisterTypeFactory(Type type, Func<IJsonReader, string, object> factory)
         {
             JsonReader._typeFactories.Set(type, factory);
         }
 
-        // Register a callback to provide a formatter for a newly encountered type
+        /// <summary>
+        /// Register a callback to provide a formatter for a newly encountered type 
+        /// </summary>
+        /// <param name="resolver">The resolver callback</param>
         public static void SetFormatterResolver(Func<Type, Action<IJsonWriter, object>> resolver)
         {
             JsonWriter._formatterResolver = resolver;
         }
 
-        // Register a callback to provide a parser for a newly encountered value type
+        /// <summary>
+        /// Register a callback to provide a parser for a newly encountered value type 
+        /// </summary>
+        /// <param name="resolver">The resolver callback</param>
         public static void SetParserResolver(Func<Type, Func<IJsonReader, Type, object>> resolver)
         {
             JsonReader._parserResolver = resolver;
         }
 
-        // Register a callback to provide a parser for a newly encountered reference type
+        /// <summary>
+        /// Register a callback to provide a parser for a newly encountered reference type 
+        /// </summary>
+        /// <param name="resolver"></param>
         public static void SetIntoParserResolver(Func<Type, Action<IJsonReader, object>> resolver)
         {
             JsonReader._intoParserResolver = resolver;
         }
 
-
-        // Resolve passed options        
         static JsonOptions ResolveOptions(JsonOptions options)
         {
             JsonOptions resolved = JsonOptions.None;

+ 16 - 2
Topten.JsonKit/JsonMemberInfo.cs

@@ -21,14 +21,28 @@ namespace Topten.JsonKit
     // Information about a field or property found through reflection
     class JsonMemberInfo
     {
+        public JsonMemberInfo()
+        {
+        }
         // The Json key for this member
         public string JsonKey;
 
         // True if should keep existing instance (reference types only)
-        public bool KeepInstance;
+        public bool KeepInstance => Attribute?.KeepInstance ?? false;
 
         // True if deprecated
-        public bool Deprecated;
+        public bool Deprecated => Attribute?.Deprecated ?? false;
+
+        // True if should be excluded when null
+        public bool ExcludeIfNull => Attribute?.ExcludeIfNull ?? false;
+
+        // The JSON attribute for the member info
+        public JsonAttribute Attribute
+        {
+            get;
+            set;
+        }
+
 
         // Reflected member info
         MemberInfo _mi;

+ 36 - 3
Topten.JsonKit/JsonOptions.cs

@@ -16,17 +16,50 @@ using System;
 
 namespace Topten.JsonKit
 {
-    // Pass to format/write/parse functions to override defaults
+    /// <summary>
+    /// Options to controls formatting/parsing and write behaviour
+    /// </summary>
     [Flags]
     public enum JsonOptions
     {
+        /// <summary>
+        /// No special options
+        /// </summary>
         None = 0,
+
+        /// <summary>
+        /// Force writing of whitespace (pretty print)
+        /// </summary>
         WriteWhitespace  = 0x00000001,
+
+        /// <summary>
+        /// Disable writing of whitespace
+        /// </summary>
         DontWriteWhitespace = 0x00000002,
+
+        /// <summary>
+        /// Enforce strict parsing (comments, array commas etc..)
+        /// </summary>
         StrictParser = 0x00000004,
+
+        /// <summary>
+        /// Relax some parsing rules
+        /// </summary>
         NonStrictParser = 0x00000008,
+
+        /// <summary>
+        /// Flush the textwriter stream when finished writing
+        /// </summary>
         Flush = 0x00000010,
-        AutoSavePreviousVersion = 0x00000020,       // Use "SavePreviousVersions" static property
-        SavePreviousVersion = 0x00000040,           // Always save previous version
+
+        /// <summary>
+        /// Use the Json.SavePreviousVersion property to control saving previous versions
+        /// </summary>
+        AutoSavePreviousVersion = 0x00000020,
+
+        /// <summary>
+        /// Always save a previous version
+        /// </summary>
+        SavePreviousVersion = 0x00000040,           
     }
 }

+ 19 - 3
Topten.JsonKit/JsonParseException.cs

@@ -16,16 +16,32 @@ using System;
 
 namespace Topten.JsonKit
 {
-    // Exception thrown for any parse error
+    /// <summary>
+    /// Exception thrown for any parse error 
+    /// </summary>
     public class JsonParseException : Exception
     {
+        /// <summary>
+        /// Constructs a new JsonParseException
+        /// </summary>
+        /// <param name="inner">The inner exception</param>
+        /// <param name="context">A string describing the context of the serialization (parent key path)</param>
+        /// <param name="position">The position in the JSON stream where the error occured</param>
         public JsonParseException(Exception inner, string context, LineOffset position) : 
             base(string.Format("JSON parse error at {0}{1} - {2}", position, string.IsNullOrEmpty(context) ? "" : string.Format(", context {0}", context), inner.Message), inner)
         {
             Position = position;
             Context = context;
         }
-        public LineOffset Position;
-        public string Context;
+
+        /// <summary>
+        /// The position in the JSON stream where the error occured
+        /// </summary>
+        public LineOffset Position { get; private set; }
+
+        /// <summary>
+        /// A string describing the context of the serialization (parent key path)
+        /// </summary>
+        public string Context { get; private set; }
     }
 }

+ 29 - 4
Topten.JsonKit/JsonReader.cs

@@ -23,6 +23,9 @@ using System.Dynamic;
 
 namespace Topten.JsonKit
 {
+    /// <summary>
+    /// Implements the IJsonReader interface
+    /// </summary>
     public class JsonReader : IJsonReader
     {
         static JsonReader()
@@ -96,6 +99,11 @@ namespace Topten.JsonKit
             });
         }
 
+        /// <summary>
+        /// Constructs a new JsonReader
+        /// </summary>
+        /// <param name="r">The input TextReader stream</param>
+        /// <param name="options">Options controlling parsing behaviour</param>
         public JsonReader(TextReader r, JsonOptions options)
         {
             _tokenizer = new Tokenizer(r, options);
@@ -106,6 +114,9 @@ namespace Topten.JsonKit
         JsonOptions _options;
         List<string> _contextStack = new List<string>();
 
+        /// <summary>
+        /// Gets the current parse context (ie: parent key path)
+        /// </summary>
         public string Context
         {
             get
@@ -156,11 +167,15 @@ namespace Topten.JsonKit
             };
         }
 
+        /// <summary>
+        /// Gets the current position in the input stream
+        /// </summary>
         public LineOffset CurrentTokenPosition
         {
             get { return _tokenizer.CurrentTokenPosition; }
         }
 
+        /// <inheritdoc />
         public Token CurrentToken
         {
             get { return _tokenizer.CurrentToken; }
@@ -171,6 +186,8 @@ namespace Topten.JsonKit
         // errors on converting to the target type are thrown before the tokenizer
         // is advanced to the next token.  This ensures error location is reported 
         // at the start of the literal, not the following token.
+
+        /// <inheritdoc />
         public object ReadLiteral(Func<object, object> converter)
         {
             _tokenizer.Check(Token.Literal);
@@ -179,11 +196,15 @@ namespace Topten.JsonKit
             return retv;
         }
 
+        /// <summary>
+        /// Checks for EOF and throws an exception if not
+        /// </summary>
         public void CheckEOF()
         {
             _tokenizer.Check(Token.EOF);
         }
 
+        /// <inheritdoc />
         public object Parse(Type type)
         {
             // Null?
@@ -378,7 +399,7 @@ namespace Topten.JsonKit
             throw new InvalidDataException(string.Format("syntax error, unexpected token {0}", _tokenizer.CurrentToken));
         }
 
-        // Parse into an existing object instance
+        /// <inheritdoc />
         public void ParseInto(object into)
         {
             if (into == null)
@@ -472,27 +493,31 @@ namespace Topten.JsonKit
             throw new InvalidOperationException(string.Format("Don't know how to parse into type '{0}'", type.FullName));
         }
 
+        /// <inheritdoc />
         public T Parse<T>()
         {
             return (T)Parse(typeof(T));
         }
 
+        /// <inheritdoc />
         public LiteralKind GetLiteralKind() 
         { 
             return _tokenizer.LiteralKind; 
         }
-            
+
+        /// <inheritdoc />
         public string GetLiteralString() 
         { 
             return _tokenizer.String; 
         }
 
+        /// <inheritdoc />
         public void NextToken() 
         { 
             _tokenizer.NextToken(); 
         }
 
-        // Parse a dictionary
+        /// <inheritdoc />
         public void ParseDictionary(Action<string> callback)
         {
             _tokenizer.Skip(Token.OpenBrace);
@@ -555,7 +580,7 @@ namespace Topten.JsonKit
             }
         }
 
-        // Parse an array
+        /// <inheritdoc />
         public void ParseArray(Action callback)
         {
             _tokenizer.Skip(Token.OpenSquare);

+ 21 - 12
Topten.JsonKit/JsonUnknownAttribute.cs

@@ -17,26 +17,35 @@ using System;
 
 namespace Topten.JsonKit
 {
-    // Apply to enum values to specify which enum value to select
-    // if the supplied json value doesn't match any.
-    // If not found throws an exception
-    // eg, any unknown values in the json will be mapped to Fruit.unknown
-    //
-    //	 [JsonUnknown(Fruit.unknown)]
-    //   enum Fruit
-    //   {
-    // 		unknown,
-    //      Apple,
-    //      Pear,
-    //	 }
+    /// <summary>
+    /// Apply to enum values to specify which enum value to select
+    /// if the supplied json value doesn't match any.
+    /// If not found throws an exception
+    /// eg, any unknown values in the json will be mapped to Fruit.unknown
+    ///
+    ///	 [JsonUnknown(Fruit.unknown)]
+    ///   enum Fruit
+    ///   {
+    /// 		unknown,
+    ///      Apple,
+    ///      Pear,
+    ///	 }
+    /// </summary>
     [AttributeUsage(AttributeTargets.Enum)]
 	public class JsonUnknownAttribute : Attribute
 	{
+        /// <summary>
+        /// Constructs a new JsonUnknown attribute
+        /// </summary>
+        /// <param name="unknownValue">The value to be used for unknown enum values</param>
 		public JsonUnknownAttribute(object unknownValue)
 		{
 			UnknownValue = unknownValue;
 		}
 
+        /// <summary>
+        /// The value to be used for unknown enum values
+        /// </summary>
 		public object UnknownValue
 		{
 			get;

+ 12 - 1
Topten.JsonKit/LineOffset.cs

@@ -14,11 +14,22 @@
 
 namespace Topten.JsonKit
 {
-    // Represents a line and character offset position in the source Json
+    /// <summary>
+    /// Represents a line and character offset position in the source Json
+    /// </summary>
     public struct LineOffset
     {
+        /// <summary>
+        /// The zero based line number
+        /// </summary>
         public int Line;
+
+        /// <summary>
+        /// The zero based character offset
+        /// </summary>
         public int Offset;
+
+        /// <inheritdoc />
         public override string ToString()
         {
             return string.Format("line {0}, character {1}", Line + 1, Offset + 1);

+ 34 - 1
Topten.JsonKit/LiteralKind.cs

@@ -14,16 +14,49 @@
 
 namespace Topten.JsonKit
 {
-    // Describes the current literal in the json stream
+    /// <summary>
+    /// Describes the current literal in the json stream
+    /// </summary>
     public enum LiteralKind
     {
+        /// <summary>
+        /// Not currently on a literal
+        /// </summary>
         None,
+
+        /// <summary>
+        /// A string literal
+        /// </summary>
         String,
+
+        /// <summary>
+        /// The value `null`
+        /// </summary>
         Null,
+
+        /// <summary>
+        /// The value `true`
+        /// </summary>
         True,
+
+        /// <summary>
+        /// The value `false`
+        /// </summary>
         False,
+
+        /// <summary>
+        /// A signed integer
+        /// </summary>
         SignedInteger,
+
+        /// <summary>
+        /// An unsigned integer
+        /// </summary>
         UnsignedInteger,
+
+        /// <summary>
+        /// A floating point value
+        /// </summary>
         FloatingPoint,
     }
 }

+ 7 - 3
Topten.JsonKit/ReflectionInfo.cs

@@ -74,8 +74,13 @@ namespace Topten.JsonKit
 
                 foreach (var jmi in Members.Where(x=>!x.Deprecated))
                 {
+                    // Exclude null?
+                    var mval = jmi.GetValue(val);
+                    if (mval == null && jmi.ExcludeIfNull)
+                        continue;
+
                     w.WriteKeyNoEscaping(jmi.JsonKey);
-                    w.WriteValue(jmi.GetValue(val));
+                    w.WriteValue(mval);
                 }
 
                 var written = val as IJsonWritten;
@@ -213,8 +218,7 @@ namespace Topten.JsonKit
                             {
                                 Member = mi,
                                 JsonKey = attr.Key ?? mi.Name.Substring(0, 1).ToLower() + mi.Name.Substring(1),
-                                KeepInstance = attr.KeepInstance,
-                                Deprecated = attr.Deprecated,
+                                Attribute = attr,
                             };
                         }
 

+ 3 - 0
Topten.JsonKit/Token.cs

@@ -17,6 +17,9 @@ using System.Reflection;
 
 namespace Topten.JsonKit
 {
+    /// <summary>
+    /// An enumeration of JSON token types
+    /// </summary>
     [Obfuscation(Exclude = true, ApplyToMembers = true)]
     public enum Token
     {

+ 12 - 1
Topten.JsonKit/Topten.JsonKit.csproj

@@ -4,12 +4,15 @@
 
   <PropertyGroup>
     <TargetFrameworks>net46;netcoreapp2.1;net5.0</TargetFrameworks>
+    <PublishRepositoryUrl>true</PublishRepositoryUrl>
+    <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)'=='Release'">
     <TtsCodeSign>True</TtsCodeSign>
+    <GenerateDocumentationFile>True</GenerateDocumentationFile>
     <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
-    <Copyright>Copyright © 2014-2020 Topten Software.  All Rights Reserved</Copyright>
+    <Copyright>Copyright © 2014-2021 Topten Software.  All Rights Reserved</Copyright>
     <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
     <PackageIcon>nuget-icon.png</PackageIcon>
     <PackageProjectUrl>https://github.com/toptensoftware/jsonkit</PackageProjectUrl>
@@ -18,4 +21,12 @@
     <RepositoryUrl>https://github.com/toptensoftware/jsonkit</RepositoryUrl>
   </PropertyGroup>
 
+  <ItemGroup>
+    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+    </PackageReference>
+  </ItemGroup>
+
+
 </Project>