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

  1. Normalize the Duration before serializing: seconds += nanos / 1_000_000_000; nanos %= 1_000_000_000 (keeping sign consistent)
  2. Use Duration.FromTimeSpan(TimeSpan) or Duration.FromDays/Hours/Minutes/Seconds/Milliseconds instead of setting Seconds/Nanos by hand
  3. If values come from the wire, re-parse with Duration.Parser which validates normalization
  4. 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

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


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)