#region License /*---------------------------------------------------------------------------------*\ Distributed under the terms of an MIT-style license: The MIT License Copyright (c) 2006-2009 Stephen M. McKamey Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \*---------------------------------------------------------------------------------*/ #endregion License using System; using System.IO; using System.ComponentModel; using System.Collections; using System.Collections.Generic; using System.Text; using System.Reflection; using System.Xml; namespace JsonFx.Json { /// /// Represents a proxy method for serialization of types which do not implement IJsonSerializable. /// /// the type for this proxy /// the JsonWriter to serialize to /// the value to serialize public delegate void WriteDelegate(JsonWriter writer, T value); /// /// Writer for producing JSON data. /// public class JsonWriter : IDisposable { #region Constants public const string JsonMimeType = "application/json"; private const string AnonymousTypePrefix = "<>f__AnonymousType"; private const string ErrorMaxDepth = "The maxiumum depth of {0} was exceeded. Check for cycles in object graph."; #endregion Constants #region Fields private readonly TextWriter writer = null; private string typeHintName = null; private bool strictConformance = true; private bool prettyPrint = false; private bool skipNullValue = false; private bool useXmlSerializationAttributes = false; private int depth = 0; private int maxDepth = 25; private string tab = "\t"; private WriteDelegate dateTimeSerializer = null; #endregion Fields #region Init /// /// Ctor. /// /// TextWriter for writing public JsonWriter(TextWriter output) { this.writer = output; } /// /// Ctor. /// /// Stream for writing public JsonWriter(Stream output) { this.writer = new StreamWriter(output, Encoding.UTF8); } /// /// Ctor. /// /// File name for writing public JsonWriter(string outputFileName) { Stream stream = new FileStream(outputFileName, FileMode.Create, FileAccess.Write, FileShare.Read); this.writer = new StreamWriter(stream, Encoding.UTF8); } /// /// Ctor. /// /// StringBuilder for appending public JsonWriter(StringBuilder output) { this.writer = new StringWriter(output, System.Globalization.CultureInfo.InvariantCulture); } #endregion Init #region Properties /// /// Gets and sets the property name used for type hinting. /// public string TypeHintName { get { return this.typeHintName; } set { this.typeHintName = value; } } /// /// Gets and sets if JSON will be formatted for human reading. /// public bool PrettyPrint { get { return this.prettyPrint; } set { this.prettyPrint = value; } } /// /// Gets and sets the string to use for indentation /// public string Tab { get { return this.tab; } set { this.tab = value; } } /// /// Gets and sets the line terminator string /// public string NewLine { get { return this.writer.NewLine; } set { this.writer.NewLine = value; } } /// /// Gets and sets the maximum depth to be serialized. /// public int MaxDepth { get { return this.maxDepth; } set { if (value < 1) { throw new ArgumentOutOfRangeException("MaxDepth must be a positive integer as it controls the maximum nesting level of serialized objects."); } this.maxDepth = value; } } /// /// Gets and sets if should use XmlSerialization Attributes. /// /// /// Respects XmlIgnoreAttribute, ... /// public bool UseXmlSerializationAttributes { get { return this.useXmlSerializationAttributes; } set { this.useXmlSerializationAttributes = value; } } /// /// Gets and sets if should conform strictly to JSON spec. /// /// /// Setting to true causes NaN, Infinity, -Infinity to serialize as null. /// public bool StrictConformance { get { return this.strictConformance; } set { this.strictConformance = value; } } public bool SkipNullValue { get { return skipNullValue; } set { skipNullValue = value; } } /// /// Gets and sets a proxy formatter to use for DateTime serialization /// public WriteDelegate DateTimeSerializer { get { return this.dateTimeSerializer; } set { this.dateTimeSerializer = value; } } /// /// Gets the underlying TextWriter. /// public TextWriter TextWriter { get { return this.writer; } } #endregion Properties #region Static Methods /// /// A fast method for serializing an object to JSON /// /// /// public static string Serialize(object value) { StringBuilder output = new StringBuilder(); using (JsonWriter writer = new JsonWriter(output)) { writer.Write(value); } return output.ToString(); } #endregion Static Methods #region Public Methods public void Write(object value) { this.Write(value, false); } protected virtual void Write(object value, bool isProperty) { if (isProperty && this.prettyPrint) { this.writer.Write(' '); } if (value == null) { this.writer.Write(JsonReader.LiteralNull); return; } if (value is IJsonSerializable) { try { if (isProperty) { this.depth++; if (this.depth > this.maxDepth) { throw new JsonSerializationException(String.Format(JsonWriter.ErrorMaxDepth, this.maxDepth)); } this.WriteLine(); } ((IJsonSerializable)value).WriteJson(this); } finally { if (isProperty) { this.depth--; } } return; } // must test enumerations before value types if (value is Enum) { this.Write((Enum)value); return; } // Type.GetTypeCode() allows us to more efficiently switch type // plus cannot use 'is' for ValueTypes Type type = value.GetType(); switch (Type.GetTypeCode(type)) { case TypeCode.Boolean: { this.Write((Boolean)value); return; } case TypeCode.Byte: { this.Write((Byte)value); return; } case TypeCode.Char: { this.Write((Char)value); return; } case TypeCode.DateTime: { this.Write((DateTime)value); return; } case TypeCode.DBNull: case TypeCode.Empty: { this.writer.Write(JsonReader.LiteralNull); return; } case TypeCode.Decimal: { // From MSDN: // Conversions from Char, SByte, Int16, Int32, Int64, Byte, UInt16, UInt32, and UInt64 // to Decimal are widening conversions that never lose information or throw exceptions. // Conversions from Single or Double to Decimal throw an OverflowException // if the result of the conversion is not representable as a Decimal. this.Write((Decimal)value); return; } case TypeCode.Double: { this.Write((Double)value); return; } case TypeCode.Int16: { this.Write((Int16)value); return; } case TypeCode.Int32: { this.Write((Int32)value); return; } case TypeCode.Int64: { this.Write((Int64)value); return; } case TypeCode.SByte: { this.Write((SByte)value); return; } case TypeCode.Single: { this.Write((Single)value); return; } case TypeCode.String: { this.Write((String)value); return; } case TypeCode.UInt16: { this.Write((UInt16)value); return; } case TypeCode.UInt32: { this.Write((UInt32)value); return; } case TypeCode.UInt64: { this.Write((UInt64)value); return; } default: case TypeCode.Object: { // all others must be explicitly tested break; } } if (value is Guid) { this.Write((Guid)value); return; } if (value is Uri) { this.Write((Uri)value); return; } if (value is TimeSpan) { this.Write((TimeSpan)value); return; } if (value is Version) { this.Write((Version)value); return; } // IDictionary test must happen BEFORE IEnumerable test // since IDictionary implements IEnumerable if (value is IDictionary) { try { if (isProperty) { this.depth++; if (this.depth > this.maxDepth) { throw new JsonSerializationException(String.Format(JsonWriter.ErrorMaxDepth, this.maxDepth)); } this.WriteLine(); } this.WriteObject((IDictionary)value); } finally { if (isProperty) { this.depth--; } } return; } if (type.GetInterface(JsonReader.TypeGenericIDictionary) != null) { throw new JsonSerializationException(String.Format(JsonReader.ErrorGenericIDictionary, type)); } // IDictionary test must happen BEFORE IEnumerable test // since IDictionary implements IEnumerable if (value is IEnumerable) { if (value is XmlNode) { this.Write((System.Xml.XmlNode)value); return; } try { if (isProperty) { this.depth++; if (this.depth > this.maxDepth) { throw new JsonSerializationException(String.Format(JsonWriter.ErrorMaxDepth, this.maxDepth)); } this.WriteLine(); } this.WriteArray((IEnumerable)value); } finally { if (isProperty) { this.depth--; } } return; } // structs and classes try { if (isProperty) { this.depth++; if (this.depth > this.maxDepth) { throw new JsonSerializationException(String.Format(JsonWriter.ErrorMaxDepth, this.maxDepth)); } this.WriteLine(); } this.WriteObject(value, type); } finally { if (isProperty) { this.depth--; } } } public virtual void WriteBase64(byte[] value) { this.Write(Convert.ToBase64String(value)); } public virtual void WriteHexString(byte[] value) { if (value == null || value.Length == 0) { this.Write(String.Empty); return; } StringBuilder builder = new StringBuilder(); // Loop through each byte of the binary data // and format each one as a hexadecimal string for (int i=0; i= '\u007F' || value[i] == '<' || value[i] == '\t' || value[i] == JsonReader.OperatorStringDelim || value[i] == JsonReader.OperatorCharEscape) { this.writer.Write(value.Substring(start, i-start)); start = i+1; switch (value[i]) { case JsonReader.OperatorStringDelim: case JsonReader.OperatorCharEscape: { this.writer.Write(JsonReader.OperatorCharEscape); this.writer.Write(value[i]); continue; } case '\b': { this.writer.Write("\\b"); continue; } case '\f': { this.writer.Write("\\f"); continue; } case '\n': { this.writer.Write("\\n"); continue; } case '\r': { this.writer.Write("\\r"); continue; } case '\t': { this.writer.Write("\\t"); continue; } default: { this.writer.Write("\\u{0:X4}", Char.ConvertToUtf32(value, i)); continue; } } } } this.writer.Write(value.Substring(start, length-start)); this.writer.Write(JsonReader.OperatorStringDelim); } #endregion Public Methods #region Primative Writer Methods public virtual void Write(bool value) { this.writer.Write(value ? JsonReader.LiteralTrue : JsonReader.LiteralFalse); } public virtual void Write(byte value) { this.writer.Write("{0:g}", value); } public virtual void Write(sbyte value) { this.writer.Write("{0:g}", value); } public virtual void Write(short value) { this.writer.Write("{0:g}", value); } public virtual void Write(ushort value) { this.writer.Write("{0:g}", value); } public virtual void Write(int value) { this.writer.Write("{0:g}", value); } public virtual void Write(uint value) { this.writer.Write("{0:g}", value); } public virtual void Write(long value) { this.writer.Write("{0:g}", value); } public virtual void Write(ulong value) { this.writer.Write("{0:g}", value); } public virtual void Write(float value) { if (this.StrictConformance && (Single.IsNaN(value) || Single.IsInfinity(value))) { this.writer.Write(JsonReader.LiteralNull); } else { this.writer.Write("{0:r}", value); } } public virtual void Write(double value) { if (this.StrictConformance && (Double.IsNaN(value) || Double.IsInfinity(value))) { this.writer.Write(JsonReader.LiteralNull); } else { this.writer.Write("{0:r}", value); } } public virtual void Write(decimal value) { this.writer.Write("{0:g}", value); } public virtual void Write(char value) { this.Write(new String(value, 1)); } public virtual void Write(TimeSpan value) { this.Write(value.Ticks); } public virtual void Write(Uri value) { this.Write(value.ToString()); } public virtual void Write(Version value) { this.Write(value.ToString()); } public virtual void Write(XmlNode value) { // TODO: auto-translate XML to JsonML this.Write(value.OuterXml); } #endregion Primative Writer Methods #region Writer Methods protected internal virtual void WriteArray(IEnumerable value) { bool appendDelim = false; this.writer.Write(JsonReader.OperatorArrayStart); this.depth++; if (this.depth > this.maxDepth) { throw new JsonSerializationException(String.Format(JsonWriter.ErrorMaxDepth, this.maxDepth)); } try { foreach (object item in value) { if (appendDelim) { this.writer.Write(JsonReader.OperatorValueDelim); } else { appendDelim = true; } this.WriteLine(); this.Write(item, false); } } finally { this.depth--; } if (appendDelim) { this.WriteLine(); } this.writer.Write(JsonReader.OperatorArrayEnd); } protected virtual void WriteObject(IDictionary value) { bool appendDelim = false; this.writer.Write(JsonReader.OperatorObjectStart); this.depth++; if (this.depth > this.maxDepth) { throw new JsonSerializationException(String.Format(JsonWriter.ErrorMaxDepth, this.maxDepth)); } try { foreach (object name in value.Keys) { if (appendDelim) { this.writer.Write(JsonReader.OperatorValueDelim); } else { appendDelim = true; } this.WriteLine(); this.Write((String)name); this.writer.Write(JsonReader.OperatorNameDelim); this.Write(value[name], true); } } finally { this.depth--; } if (appendDelim) { this.WriteLine(); } this.writer.Write(JsonReader.OperatorObjectEnd); } protected virtual void WriteObject(object value, Type type) { bool appendDelim = false; this.writer.Write(JsonReader.OperatorObjectStart); this.depth++; if (this.depth > this.maxDepth) { throw new JsonSerializationException(String.Format(JsonWriter.ErrorMaxDepth, this.maxDepth)); } try { if (!String.IsNullOrEmpty(this.TypeHintName)) { if (appendDelim) { this.writer.Write(JsonReader.OperatorValueDelim); } else { appendDelim = true; } this.WriteLine(); this.Write(this.TypeHintName); this.writer.Write(JsonReader.OperatorNameDelim); this.Write(type.FullName+", "+type.Assembly.GetName().Name, true); } bool anonymousType = type.IsGenericType && type.Name.StartsWith(JsonWriter.AnonymousTypePrefix); // serialize public properties PropertyInfo[] properties = type.GetProperties(); foreach (PropertyInfo property in properties) { if (!property.CanRead) { continue; } if (!property.CanWrite && !anonymousType) { continue; } if (this.IsIgnored(type, property, value)) { continue; } object propertyValue = property.GetValue(value, null); if ((propertyValue == null) && SkipNullValue) { continue; } if (this.IsDefaultValue(property, propertyValue)) { continue; } if (appendDelim) { this.writer.Write(JsonReader.OperatorValueDelim); } else { appendDelim = true; } string propertyName = JsonNameAttribute.GetJsonName(property); if (String.IsNullOrEmpty(propertyName)) { propertyName = property.Name; } this.WriteLine(); this.Write(propertyName); this.writer.Write(JsonReader.OperatorNameDelim); this.Write(propertyValue, true); } // serialize public fields FieldInfo[] fields = type.GetFields(); foreach (FieldInfo field in fields) { if (!field.IsPublic || field.IsStatic) { continue; } if (this.IsIgnored(type, field, value)) { continue; } object fieldValue = field.GetValue(value); if (this.IsDefaultValue(field, fieldValue)) { continue; } if (appendDelim) { this.writer.Write(JsonReader.OperatorValueDelim); this.WriteLine(); } else { appendDelim = true; } string fieldName = JsonNameAttribute.GetJsonName(field); if (String.IsNullOrEmpty(fieldName)) { fieldName = field.Name; } // use Attributes here to control naming this.Write(fieldName); this.writer.Write(JsonReader.OperatorNameDelim); this.Write(fieldValue, true); } } finally { this.depth--; } if (appendDelim) { this.WriteLine(); } this.writer.Write(JsonReader.OperatorObjectEnd); } protected virtual void WriteLine() { if (!this.prettyPrint) { return; } this.writer.WriteLine(); for (int i=0; i /// Determines if the property or field should not be serialized. /// /// /// /// /// /// /// Checks these in order, if any returns true then this is true: /// - is flagged with the JsonIgnoreAttribute property /// - has a JsonSpecifiedProperty which returns false /// private bool IsIgnored(Type objType, MemberInfo member, object obj) { if (JsonIgnoreAttribute.IsJsonIgnore(member)) { return true; } string specifiedProperty = JsonSpecifiedPropertyAttribute.GetJsonSpecifiedProperty(member); if (!String.IsNullOrEmpty(specifiedProperty)) { PropertyInfo specProp = objType.GetProperty(specifiedProperty); if (specProp != null) { object isSpecified = specProp.GetValue(obj, null); if (isSpecified is Boolean && !Convert.ToBoolean(isSpecified)) { return true; } } } if (this.UseXmlSerializationAttributes) { if (JsonIgnoreAttribute.IsXmlIgnore(member)) { return true; } PropertyInfo specProp = objType.GetProperty(member.Name+"Specified"); if (specProp != null) { object isSpecified = specProp.GetValue(obj, null); if (isSpecified is Boolean && !Convert.ToBoolean(isSpecified)) { return true; } } } return false; } /// /// Determines if the member value matches the DefaultValue attribute /// /// if has a value equivalent to the DefaultValueAttribute private bool IsDefaultValue(MemberInfo member, object value) { DefaultValueAttribute attribute = Attribute.GetCustomAttribute(member, typeof(DefaultValueAttribute)) as DefaultValueAttribute; if (attribute == null) { return false; } if (attribute.Value == null) { return (value == null); } return (attribute.Value.Equals(value)); } #region GetFlagList /// /// Splits a bitwise-OR'd set of enums into a list. /// /// the enum type /// the combined value /// list of flag enums /// /// from PseudoCode.EnumHelper /// private static Enum[] GetFlagList(Type enumType, object value) { ulong longVal = Convert.ToUInt64(value); Array enumValues = Enum.GetValues(enumType); List enums = new List(enumValues.Length); // check for empty if (longVal == 0L) { // Return the value of empty, or zero if none exists if (Convert.ToUInt64(enumValues.GetValue(0)) == 0L) enums.Add(enumValues.GetValue(0) as Enum); else enums.Add(null); return enums.ToArray(); } for (int i = enumValues.Length-1; i >= 0; i--) { ulong enumValue = Convert.ToUInt64(enumValues.GetValue(i)); if ((i == 0) && (enumValue == 0L)) continue; // matches a value in enumeration if ((longVal & enumValue) == enumValue) { // remove from val longVal -= enumValue; // add enum to list enums.Add(enumValues.GetValue(i) as Enum); } } if (longVal != 0x0L) enums.Add(Enum.ToObject(enumType, longVal) as Enum); return enums.ToArray(); } #endregion GetFlagList #endregion Private Methods #region Utility Methods /// /// Verifies is a valid EcmaScript variable expression. /// /// the variable expression /// varExpr public static string EnsureValidIdentifier(string varExpr, bool nested) { return JsonWriter.EnsureValidIdentifier(varExpr, nested, true); } /// /// Verifies is a valid EcmaScript variable expression. /// /// the variable expression /// varExpr /// /// http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf /// /// IdentifierName = /// IdentifierStart | IdentifierName IdentifierPart /// IdentifierStart = /// Letter | '$' | '_' /// IdentifierPart = /// IdentifierStart | Digit /// public static string EnsureValidIdentifier(string varExpr, bool nested, bool throwOnEmpty) { if (String.IsNullOrEmpty(varExpr)) { if (throwOnEmpty) { throw new ArgumentException("Variable expression is empty."); } return String.Empty; } varExpr = varExpr.Replace(" ", ""); bool indentPart = false; // TODO: ensure not a keyword foreach (char ch in varExpr) { if (indentPart) { if (ch == '.' && nested) { // reset to IndentifierStart indentPart = false; continue; } if (Char.IsDigit(ch)) { continue; } } // can be start or part if (Char.IsLetterOrDigit(ch) || ch == '_' || ch == '$'||ch=='-') { indentPart = true; continue; } throw new ArgumentException("Variable expression \""+varExpr+"\" is not supported."); } return varExpr; } #endregion Utility Methods #region IDisposable Members void IDisposable.Dispose() { if (this.writer != null) { this.writer.Dispose(); } } #endregion IDisposable Members } }