Newtonsoft.json: Correct ISO8601 TimeSpan format

Created on 30 Mar 2016  Â·  5Comments  Â·  Source: JamesNK/Newtonsoft.Json

Hi,

first of all, thanks for your great library! I have one issue, though: According to Wikipedia, ISO8601 durations should be written in the form of e.g. PT8H5M25S for a span of 8 hours, 5 minutes and 25 seconds. However, Newtonsoft JSON refuses to deserialize this format into a timespan, and serializes it to 08:05:25.

Most helpful comment

Using the following converter solves the issue for me:

public class TimespanConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(TimeSpan);
        }

        public override bool CanRead => true;
        public override bool CanWrite => true;



        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (objectType != typeof(TimeSpan))
                throw new ArgumentException();

            var spanString = reader.Value as string;
            if (spanString == null)
                return null;
            return XmlConvert.ToTimeSpan(spanString);
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var duration = (TimeSpan) value;
            writer.WriteValue(XmlConvert.ToString(duration));
        }
    }

I guess something similar should be built into Json.NET.

All 5 comments

Using the following converter solves the issue for me:

public class TimespanConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(TimeSpan);
        }

        public override bool CanRead => true;
        public override bool CanWrite => true;



        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (objectType != typeof(TimeSpan))
                throw new ArgumentException();

            var spanString = reader.Value as string;
            if (spanString == null)
                return null;
            return XmlConvert.ToTimeSpan(spanString);
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var duration = (TimeSpan) value;
            writer.WriteValue(XmlConvert.ToString(duration));
        }
    }

I guess something similar should be built into Json.NET.

Yes it would be a great feature, especially as momentjs serialize its durations with the ISO8601 format.

I have coded below JsonConverter for TimeSpan/TimeSpan? properties, if someone need

```
///


/// Converts a (omits Day part) to and from a ISO 8601 Duration-Time .
///

public class IsoTimeSpanConverter : JsonConverter
{
///
/// Writes the JSON representation of the object.
///

/// The to write to.
/// The value.
/// The calling serializer.
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{

        if (value == null)
        {
            writer.WriteNull();
            return;
        }

        var isValueTimeSpan = value is TimeSpan;

        if (!isValueTimeSpan)
        {
            throw new JsonSerializationException("Expected TimeSpan object value.");
        }

        var timeSpan = (TimeSpan)value;

        var text = string.Format("PT{0}H{1}M{2}S", timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds);
        writer.WriteValue(text);
    }

    private bool IsNullableType(Type objectType)
    {
        return objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(Nullable<>);
    }

    /// <summary>
    /// Reads the JSON representation of the object.
    /// </summary>
    /// <param name="reader">The <see cref="JsonReader"/> to read from.</param>
    /// <param name="objectType">Type of the object.</param>
    /// <param name="existingValue">The existing property value of the JSON that is being converted.</param>
    /// <param name="serializer">The calling serializer.</param>
    /// <returns>The object value.</returns>
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        bool isNullable = IsNullableType(objectType);

        if (reader.TokenType == JsonToken.Null)
        {
            if (!isNullable)
            {
                throw new JsonSerializationException(string.Format("Cannot convert null value to {0}.", objectType));
            }

            return null;
        }

        Type t = isNullable ? Nullable.GetUnderlyingType(objectType) : objectType;


        if (reader.TokenType == JsonToken.String)
        {
            var timeSpanText = reader.Value.ToString();

            if (!timeSpanText.StartsWith("PT"))
            {
                throw new JsonSerializationException("TimeSpan text should strat with PT");
            }

            if (!timeSpanText.Contains("H"))
            {
                throw new JsonSerializationException("TimeSpan text should contain H");
            }
            if (!timeSpanText.Contains("M"))
            {
                throw new JsonSerializationException("TimeSpan text should contain M");
            }
            if (!timeSpanText.Contains("S"))
            {
                throw new JsonSerializationException("TimeSpan text should contain S");
            }

            var indexOfH = timeSpanText.IndexOf("H");
            var indexOfM = timeSpanText.IndexOf("M");
            var indexOfS = timeSpanText.IndexOf("S");

            int hours;

            if (!Int32.TryParse(timeSpanText.Substring(2, indexOfH - 2), out hours))
            {
                throw new JsonSerializationException("H could not be converted to Int32");
            }

            int minutes;
            if (!Int32.TryParse(timeSpanText.Substring(indexOfH + 1, indexOfM - indexOfH - 1), out minutes))
            {
                throw new JsonSerializationException("M could not be converted to Int32");
            }

            int seconds;
            if (!Int32.TryParse(timeSpanText.Substring(indexOfM + 1, indexOfS - indexOfM - 1), out seconds))
            {
                throw new JsonSerializationException("S could not be converted to Int32");
            }

            return new TimeSpan(hours, minutes, seconds);
        }


        throw new JsonSerializationException(string.Format("Unexpected token {0} when parsing TimeSpan.", reader.TokenType));
    }

    /// <summary>
    /// Determines whether this instance can convert the specified object type.
    /// </summary>
    /// <param name="objectType">Type of the object.</param>
    /// <returns>
    /// <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
    /// </returns>
    public override bool CanConvert(Type objectType)
    {

        Type t = IsNullableType(objectType) ? Nullable.GetUnderlyingType(objectType) : objectType;

        return typeof(TimeSpan) == t;
    }
}

```

@SGStino you are definitely right for your sample but the above code works as it should be.

If you check the summary section of the IsoTimeSpanConverter you will see the indication that it omits the Day part (also Months and years).

Above code is just for sharing the idea, everyone is free to use and develop it for their needs.

Before copying one of the both code snippets above, keep in mind that both are not 100% ISO8601 compliant. âš 

The solution by @supermihi does use XmlConvert. This implementation is based on xs:duration of the w3c XSD recommendation which again is based on ISO8601, but not fully compliant. Parsing a valid ISO duration PT-5H fails as it expects negative durations to be formatted as -PT5H.

The code of @staviloglu does not work with days as it enforces a ISO timespan to start with PT and enforce "H", "M" and "S" to be in the string. P5D (5 days) is also valid and does not work.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

schani picture schani  Â·  11Comments

XeonG picture XeonG  Â·  12Comments

azzlack picture azzlack  Â·  38Comments

plokin picture plokin  Â·  12Comments

davidfowl picture davidfowl  Â·  49Comments