In lesson 1.3.4, you got to know the .NET types for days, times of day, points in time and durations, and the time zone Europe/Berlin with its two clock changes. On the web, two things are added. First, times travel as text through JSON, and both sides have to understand the same text the same way. Second, there are suddenly three clocks: the office’s in Berlin, the visitor’s, who may be sitting in New York, and the server’s, which runs in a container and mostly shows UTC. Which clock a time means has to be in the data. Otherwise, chance decides where the server happens to be running.
In this lesson, you learn what times look like in JSON, which value of an API gets which type, and why a point in time without an offset should be rejected. At the end, the Kladde can create appointments through the API and outputs all times in Berlin time with an offset, even on the night of the clock change. As in lesson 1.3.4, the examples use the year 2025: The clocks were moved forward from 02:00 to 03:00 on March 30 and back from 03:00 to 02:00 on October 26.
Times in JSON
JSON knows numbers, text, Boolean values, lists and objects, but no type for a date or a time. A time is therefore written into it as
text, and the form of that text is governed by the standard ISO 8601: first the year, then month and day, then a T and the time of
day, and last the distance from UTC. System.Text.Json, which ASP.NET Core uses for JSON, writes and reads exactly this form.
In k2-practice/2-3, create the file times.cs:
// times.cs: times in a web API
#:sdk Microsoft.NET.Sdk.Web
#:property PublishAot=false
var builder = WebApplication.CreateBuilder(args);
builder.Logging.SetMinimumLevel(LogLevel.Warning);
var app = builder.Build();
TimeZoneInfo berlin = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin");
app.MapGet("/api/formats", () => new TimeFormats(
Moment: new DateTimeOffset(2025, 3, 30, 3, 30, 0, TimeSpan.FromHours(2)),
MomentInUtc: new DateTimeOffset(2025, 3, 30, 1, 30, 0, TimeSpan.Zero),
Unclear: new DateTime(2025, 3, 30, 3, 30, 0),
Day: new DateOnly(2025, 3, 30),
Opens: new TimeOnly(9, 0),
Length: TimeSpan.FromMinutes(90)));
app.MapPost("/api/read", (TimeInput input) => new { input.At, InUtc = input.At.ToUniversalTime() });
app.MapGet("/api/day", (DateOnly day) =>
{
DateTimeOffset starts = Midnight(day, berlin);
DateTimeOffset ends = Midnight(day.AddDays(1), berlin);
return new { day, starts, ends, hours = (ends - starts).TotalHours };
});
app.Run();
static DateTimeOffset Midnight(DateOnly day, TimeZoneInfo zone)
{
DateTime local = day.ToDateTime(TimeOnly.MinValue);
return new DateTimeOffset(local, zone.GetUtcOffset(local));
}
record TimeFormats(DateTimeOffset Moment, DateTimeOffset MomentInUtc, DateTime Unclear, DateOnly Day, TimeOnly Opens, TimeSpan Length);
record TimeInput(DateTimeOffset At);
Start the app with dotnet run times.cs and ask for the formats in a second terminal:
curl http://localhost:5000/api/formats
{"moment":"2025-03-30T03:30:00+02:00","momentInUtc":"2025-03-30T01:30:00+00:00","unclear":"2025-03-30T03:30:00","day":"2025-03-30","opens":"09:00:00","length":"01:30:00"}
This is how you read the six values:
momentis a point in time with its distance from UTC, the offset+02:00. Whoever reads it knows exactly which second is meant and also sees the time of day that a clock at that place showed.momentInUtcis the same point in time in UTC. ADateTimeOffsetwith the offset zero writes+00:00; elsewhere you often see aZat the end, which also means UTC. System.Text.Json reads both forms.unclearis aDateTimewith no statement about where it applies (itsKindisUnspecified, lesson 1.3.4). The offset is therefore missing in the JSON, and nobody can say which point in time is meant.dayis aDateOnly: only the calendar day, without a time of day and without a zone.opensis aTimeOnly, a time of day as it hangs on the wall, with seconds.lengthis aTimeSpanin the form hours, minutes, seconds. Here,01:30:00means “an hour and a half” and still looks like “1:30 in the morning”. For a duration, the Kladde therefore uses whole minutes as a number:"minutes": 90.
The order year, month, day has a nice side effect: Texts in this form can be sorted as text, and they then also stand in the right chronological order.