I have a need to format the output json of a decimal to a currency, with the culture specified my the object I am serializing, the object could be nested so I cannot preset
What you want to do is to intercept and modify the value of a specific property of an object as it is being serialized while using default serialization for all other properties. This can be done with a custom ContractResolver that replaces the ValueProvider of the property in question when a specific attribute is applied.
First, define the following attribute and contract resolver:
[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field, AllowMultiple = false)]
public class JsonFormatAttribute : System.Attribute
{
public JsonFormatAttribute(string formattingString)
{
this.FormattingString = formattingString;
}
///
/// The format string to pass to string.Format()
///
public string FormattingString { get; set; }
///
/// The name of the underlying property that returns the object's culture, or NULL if not applicable.
///
public string CulturePropertyName { get; set; }
}
public class FormattedPropertyContractResolver : DefaultContractResolver
{
protected override IList CreateProperties(Type type, MemberSerialization memberSerialization)
{
return base.CreateProperties(type, memberSerialization)
.AddFormatting();
}
}
public static class JsonContractExtensions
{
class FormattedValueProvider : IValueProvider
{
readonly IValueProvider baseProvider;
readonly string formatString;
readonly IValueProvider cultureValueProvider;
public FormattedValueProvider(IValueProvider baseProvider, string formatString, IValueProvider cultureValueProvider)
{
this.baseProvider = baseProvider;
this.formatString = formatString;
this.cultureValueProvider = cultureValueProvider;
}
#region IValueProvider Members
public object GetValue(object target)
{
var value = baseProvider.GetValue(target);
var culture = cultureValueProvider == null ? null : (CultureInfo)cultureValueProvider.GetValue(target);
return string.Format(culture ?? CultureInfo.InvariantCulture, formatString, value);
}
public void SetValue(object target, object value)
{
// This contract resolver should only be used for serialization, not deserialization, so throw an exception.
throw new NotImplementedException();
}
#endregion
}
public static IList AddFormatting(this IList properties)
{
ILookup lookup = null;
foreach (var jsonProperty in properties)
{
var attr = (JsonFormatAttribute)jsonProperty.AttributeProvider.GetAttributes(typeof(JsonFormatAttribute), false).SingleOrDefault();
if (attr != null)
{
IValueProvider cultureValueProvider = null;
if (attr.CulturePropertyName != null)
{
if (lookup == null)
lookup = properties.ToLookup(p => p.UnderlyingName);
var cultureProperty = lookup[attr.CulturePropertyName].FirstOrDefault();
if (cultureProperty != null)
cultureValueProvider = cultureProperty.ValueProvider;
}
jsonProperty.ValueProvider = new FormattedValueProvider(jsonProperty.ValueProvider, attr.FormattingString, cultureValueProvider);
jsonProperty.PropertyType = typeof(string);
}
}
return properties;
}
}
Next, define your object as follows:
public class RootObject
{
[JsonFormat("{0:c}", CulturePropertyName = nameof(Culture))]
public decimal Cost { get; set; }
[JsonIgnore]
public CultureInfo Culture { get; set; }
public string SomeValue { get; set; }
public string SomeOtherValue { get; set; }
}
Finally, serialize as follows:
var settings = new JsonSerializerSettings
{
ContractResolver = new FormattedPropertyContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy(),
},
};
var json = JsonConvert.SerializeObject(root, Formatting.Indented, settings);
Notes:
Since you are not serializing the culture name, I can't see any way to deserialize the Cost property. Thus I threw an exception from the SetValue method.
(And, even if you were serializing the culture name, since a JSON object is an unordered set of name/value pairs according the standard, there's no way to guarantee the culture name appears before the cost in the JSON being deserialized. This may be related to why Newtonsoft does not provide access to the parent stack. During deserialization there's no guarantee that required properties in the parent hierarchy have been read - or even that the parents have been constructed.)
If you have to apply several different customization rules to your contracts, consider using ConfigurableContractResolver from How to add metadata to describe which properties are dates in JSON.Net.
You may want to cache the contract resolver for best performance.
Another approach would be to add a converter to the parent object that generates a default serialization to JObject by disabling itself temporarily, tweaks the returned JObject, then writes that out. For examples of this approach see JSON.Net throws StackOverflowException when using [JsonConvert()] or Can I serialize nested properties to my class in one operation with Json.net?.
In comments you write, Inside WriteJson I cannot figure out how to access the parent object and it's properties. It should be possible to do this with a custom IValueProvider that returns a Tuple or similar class containing the parent and the value, which would be used in concert with a specific JsonConverter that expects such input. Not sure I'd recommend this though since it's extremely tricky.
Working sample .Net fiddle.