YAML to C# Converter

A YAML file carries no types, so a C# class for one is a set of guesses. This page makes those guesses in the open: every number picks int, long or double from its value, every list of maps is folded into one shape, and every key that had to be renamed or made nullable is listed under the output with the reason. Pick the serializer first, because the attributes on the class depend on which one reads the file.

YAML to C# class generator

Read by
.yaml
Samples
C#
Class shape

C# appears here as you type.

0 classes0 properties0 attributes0 warnings

Pick the reader first, because the attributes follow from it

Four different libraries end up reading YAML into C# objects, and they disagree on how a key finds its property. System.Text.Json compares names exactly, so port does not reach Port without help. Newtonsoft ignores case on its own. YamlDotNet compares exactly unless you hand the deserializer a naming convention. The configuration binder behind IOptions<T> ignores case but has no idea what to do with a hyphen.

The Read by control above changes the attribute written on each property to match, and skips the attribute wherever the library would find the key by itself.

Read byAttribute writtenMatches on its ownWhere the YAML comes from
System.Text.Json[JsonPropertyName("base-url")] on every property whose key is not already the property nameExact name onlyYAML converted to JSON first, or a YAML file loaded through a converter into JsonSerializer
Newtonsoft[JsonProperty("base-url")] only where the key differs by more than caseCase-insensitiveSame as above, in projects still on Json.NET
YamlDotNet[YamlMember(Alias = "base-url")] on every property whose key is not already the property nameExact name, or a naming convention set on the deserializerYAML read directly with DeserializerBuilder
IConfiguration[ConfigurationKeyName("base-url")] only where the key differs by more than caseCase-insensitiveA YAML configuration provider feeding builder.Configuration, bound with Get<T>() or the options pattern
No attributesNoneDepends on the callerA class you will annotate by hand, or a file whose keys already match C# names

When every key in a file follows one convention, the attributes are not the best answer. The log says so: a file that is kebab-case throughout gets a note pointing at JsonNamingPolicy.KebabCaseLower for System.Text.Json on .NET 8 or later, HyphenatedNamingConvention for YamlDotNet, or KebabCaseNamingStrategy for Newtonsoft. One line on the serializer replaces every attribute in the class, and the class stops caring how the file spells its keys. The first sample mixes listen_port in with hyphenated keys on purpose, so that note stays quiet and the attributes stay.

How a YAML number picks int, long, double or decimal

YAML has one number type. C# has around a dozen. The generator reads the literal, not the intent, so a whole number inside plus or minus 2,147,483,647 is int and anything past that is long. A value with a decimal point or an exponent is double, or decimal when the first checkbox is on. Turn that on for money and rates. 0.0025 as a double is not 0.0025, and a per-call price that drifts in the fourth decimal place is the kind of bug that appears on an invoice months later.

YAML in
listen_port: 8080
max-body-bytes: 4294967296
price-per-call: 0.0025
api-version: 1.10
release-date: 2026-09-14
C# out, decimal and dates on
public int ListenPort { get; set; }
public long MaxBodyBytes { get; set; }
public decimal PricePerCall { get; set; }
public decimal ApiVersion { get; set; }
public DateTime ReleaseDate { get; set; }

The fourth line is the one to look at. api-version: 1.10 is unquoted, so every YAML parser reads it as the number 1.1, and the property comes out numeric. The log flags the line. The fix is in the file, not the class: write "1.10" with quotes and the property becomes string, which is what a version usually needs to be. The same trap catches build: 010, which is octal in YAML 1.1 and decimal in 1.2 depending on the parser, so quote that one too.

Dates get the same treatment in the other direction. An unquoted 2026-09-14 is recognised as a date while the file is read, but the property stays string unless the Dates as DateTime box is ticked, because nothing on the .NET side agrees on what a bare date becomes. With the box on, a date or a timestamp without an offset is DateTime, and a timestamp that carries Z or +05:00 is DateTimeOffset, which keeps the offset instead of silently shifting the value to the machine's local zone.

Nullable comes from evidence, not from a checkbox

A property is written as bool? or string? in three cases only. The key is missing from some entries in a list of maps, like readonly on one replica out of two in the first sample. The value is ~ or null somewhere in the file. Or the same key holds a string in one place and a number in another, which types as object? with a warning, since no single C# type covers both. Every other property is non-nullable, and the log explains each exception with the count it came from, 1 of 2 entries or 3 of 4, so you decide whether the sample was representative or the field is optional for real.

Non-nullable has a cost under #nullable enable, which the output turns on at the top of the file. A plain public string Name { get; set; } earns warning CS8618 from the compiler, because nothing guarantees the property holds a value before something reads it. The class and record shapes answer that with initializers, = ""; on strings and = new(); on lists, dictionaries and nested classes.

The required members shape answers it differently: no initializers, and the required keyword on every non-nullable property instead. That needs C# 11, and only System.Text.Json on .NET 7 or later treats the keyword as a deserialization rule, throwing when a required key is absent. Newtonsoft sees the keyword at compile time only and leaves the property at its default when the key is missing, so pair the shape with [JsonProperty(Required = Required.Always)] there if you want the same guarantee.

What the configuration binder will and will not do

Most YAML that reaches a .NET service is settings, and settings usually arrive through IConfiguration rather than a serializer. There is no YAML provider in the box, so the file is loaded with a package such as NetEscapades.Configuration.Yaml, after which builder.Configuration.GetSection("service").Get<Service>() or services.Configure<Service>(section) binds the section onto the class. The binder matches property names to keys ignoring case and nothing else, so base-url never lands on BaseUrl by itself. Since .NET 6 the fix is [ConfigurationKeyName("base-url")] from Microsoft.Extensions.Configuration, and the IConfiguration option writes exactly those, on exactly the properties that need one.

Two things the binder does that the class cannot express. Any scalar is bound through TypeConverter, so "true" in quotes and bare true both reach a bool, which is why the second sample quotes its environment values without changing the class. And a list is bound from keys named 0, 1, 2, which is how a YAML sequence gets flattened into configuration keys, so List<string> works but a string[] would too.

Where this reader stops

A JSON response from an API fits the JSON to C# converter better, since that page infers from response shapes rather than config files. XML settings go through XML to C#, which understands attributes and repeated elements. And a YAML file that will not read here at all deserves a pass through the YAML validator first, which lists every problem in the file rather than stopping at the first one.

Questions from the first real file

Answered from the cases the log flags most often.

Why is one number long when the rest are int?

Because its value is past 2,147,483,647, the top of the int range. The generator sizes each whole number from its literal, so a byte limit like 4294967296 comes out long while a port stays int. If a field is int in the sample but will grow past that, widen the type by hand. The generator sees one file and cannot know what future values look like.

My version 1.10 came out as a double. What happened?

An unquoted 1.10 is a number in YAML, and every parser reads it as 1.1 before this page ever sees the text. The log flags the line. Quote the value in the file, "1.10", and the property becomes string. This is a change to the YAML, not to the class, and the same rule applies to anything else that looks numeric but is an identifier, like a build number with a leading zero.

Why does System.Text.Json get an attribute on every property but Newtonsoft only on some?

System.Text.Json matches property names exactly by default, so even port versus Port needs [JsonPropertyName("port")] or a case-insensitive option on the serializer. Newtonsoft ignores case on its own, so the attribute only appears where the key differs by more than case, such as base-url. The same logic applies to YamlDotNet, which is exact, and the configuration binder, which is case-insensitive.

Can I bind this class with builder.Configuration.GetSection("service").Get<Service>()?

Yes, once the YAML is loaded into IConfiguration by a provider package, since .NET ships none for YAML. Pick the IConfiguration option above so hyphenated and underscored keys get [ConfigurationKeyName] attributes. Without them the binder matches names ignoring case and nothing else, and a key like base-url silently leaves BaseUrl at its default.

The compiler warns CS8618 on the generated class. Is the output wrong?

The class and record shapes ship with initializers on every non-nullable reference property, = "" and = new(), which satisfy that warning. If you see CS8618, the file was edited after generation or the initializers were removed. The alternative is the required members shape, which drops the initializers and marks each non-nullable property required instead, and that needs C# 11 or later.

Why did readonly on the Replica class come out nullable?

The two replicas in the first sample do not carry the same keys. One has readonly, one does not. The generator folds every entry in a list into one shape, and a key missing from any entry becomes nullable, since a non-nullable bool would read as false for the entry that never set it and hide the difference between absent and false. The log names the count, 1 of 2 entries, so you know whether the sample was complete.

What happened to my <<: *defaults merge lines?

They were skipped, each with a warning in the log naming the line. The reader does not keep anchored blocks around to merge later, so the keys from the anchored map are missing from the class that used the merge. Run the file through YAML to JSON, which expands anchors and merges, then feed the JSON to the JSON to C# converter to get the full shape.

Is the file uploaded anywhere?

No. Reading the YAML and writing the C# both happen in JavaScript inside this page after it loads. Nothing is sent to a server, nothing is stored between visits, and closing the tab clears both panes.