XINCGer/Unity3DTraining · error · InvalidOperationException
Non-normalized duration value
Error message
Non-normalized duration value
What it means
Duration.ToJson refuses to serialize a Duration whose Seconds/Nanos are not normalized (nanos outside [-999999999, 999999999] with mismatched signs, or both zero-signed wrongly). The JSON mapping for Duration requires a canonical 'Ns' string, so the library throws InvalidOperationException instead of emitting invalid protobuf-JSON. Note ToDiagnosticString catches this path and emits a warning JSON only for out-of-range values, but non-normalized values still throw.
Solutions
- Normalize the Duration before serializing: seconds += nanos / 1_000_000_000; nanos %= 1_000_000_000 (keeping sign consistent)
- Use Duration.FromTimeSpan(TimeSpan) or Duration.FromDays/Hours/Minutes/Seconds/Milliseconds instead of setting Seconds/Nanos by hand
- If values come from the wire, re-parse with Duration.Parser which validates normalization
- Catch InvalidOperationException in the ToString/serialization path and log the raw Seconds/Nanos for diagnosis
Example fix
// before
var d = new Duration { Seconds = 3, Nanos = 1_500_000_000 };
var s = d.ToDiagnosticString(); // throws
// after
var d = new Duration { Seconds = 4, Nanos = 500_000_000 };
// or programmatically:
long totalNanos = 3L * 1_000_000_000 + 1_500_000_000;
var d2 = new Duration { Seconds = totalNanos / 1_000_000_000, Nanos = (int)(totalNanos % 1_000_000_000) };
var s2 = d2.ToDiagnosticString(); // "4.500000000s" Defensive patterns
Strategy: validation
Validate before calling
static bool IsNormalizedDuration(Duration d) => d.Nanos >= -999_999_999 && d.Nanos <= 999_999_999 && (d.Seconds < 0 ? d.Nanos <= 0 : d.Seconds > 0 ? d.Nanos >= 0 : true);
Type guard
static bool IsValidDuration(Duration d) => d != null && d.Nanos is >= -999_999_999 and <= 999_999_999;
Try / catch
try { return d.ToDiagnosticString(); } catch (InvalidOperationException ex) { log.Warn(ex, "Non-normalized duration: {Seconds}.{Nanos}", d.Seconds, d.Nanos); return $"{d.Seconds}s {d.Nanos}ns (non-normalized)"; } Prevention
- Always build Durations with FromTimeSpan/FromDays/FromHours/... helpers
- Renormalize after any manual Seconds/Nanos arithmetic
- Validate values at deserialization boundaries before use
When it happens
Trigger: Calling ToDiagnosticString()/ToJson() on a Duration constructed or deserialized with non-normalized values, e.g. Nanos=1500000000 (should be Seconds+=1, Nanos=500000000) or Seconds=1, Nanos=-500000000 (mixed signs).
Common situations: Manually building Duration objects with hand-computed nanos; doing arithmetic on Seconds/Nanos fields without renormalizing; migrating data from another protobuf implementation that didn't normalize; decoding durations from non-Google middleware that skips validation.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Invalid field mask to be converted to JSON
- Non-normalized timestamp value
- Did not write as much data as expected.
- Expected string value for Duration
- Invalid Duration value
AI-assisted analysis of XINCGer/Unity3DTraining@016f98412e (2026-09-12).
Data as JSON: /api/errors/38d5b8b4928967a2.
Report an issue: GitHub.
Appendix: source
Thrown at NetWorkAndResources/Socket_Protobuff/Assets/Plugins/Google.Protobuf/WellKnownTypes/DurationPartial.cs:224
builder.Append('-');
}
builder.Append(seconds.ToString("d", CultureInfo.InvariantCulture));
AppendNanoseconds(builder, Math.Abs(nanoseconds));
builder.Append("s\"");
return builder.ToString();
}
if (diagnosticOnly)
{
// Note: the double braces here are escaping for braces in format strings.
return string.Format(CultureInfo.InvariantCulture,
"{{ \"@warning\": \"Invalid Duration\", \"seconds\": \"{0}\", \"nanos\": {1} }}",
seconds,
nanoseconds);
}
else
{
throw new InvalidOperationException("Non-normalized duration value");
}
}
/// <summary>
/// Returns a string representation of this <see cref="Duration"/> for diagnostic purposes.
/// </summary>
/// <remarks>
/// Normally the returned value will be a JSON string value (including leading and trailing quotes) but
/// when the value is non-normalized or out of range, a JSON object representation will be returned
/// instead, including a warning. This is to avoid exceptions being thrown when trying to
/// diagnose problems - the regular JSON formatter will still throw an exception for non-normalized
/// values.
/// </remarks>
/// <returns>A string representation of this value.</returns>
public string ToDiagnosticString()
{
return ToJson(Seconds, Nanos, true);
}View on GitHub (pinned to 016f98412e)