ZetCode

C# Object.ToString

last modified October 17, 2026

In this article we show how to use the Object.ToString method in C#.

The Object.ToString method returns a string that represents the current object. It is the main formatting method in .NET. The default implementation of the method returns the fully qualified name of the object's type.

The ToString method is called implicitly in many places: when we pass an object to the Console.WriteLine method, when we use it in an interpolated string, or when we concatenate it with a string.

There are multiple challenges when converting types into strings. For instance, the string output may be culture specific, as seen in dates, times, and numbers. Or the internal implementation of the type differs from the format the user expects.

Custom types often override the default implementation of the Object.ToString method. In addition, it is possible to overload the method. For maximum flexibility, a type can implement the IFormattable interface and support format strings and culture-specific formatting.

C# default ToString

The default ToString method returns the fully qualified name of the type.

Program.cs
var u = new Users.User("John Doe", "gardener");
var o = new object();
int[] vals = [1, 2, 3];
List<string> words = ["sky", "blue"];

Console.WriteLine(u);
Console.WriteLine(o);
Console.WriteLine(vals);
Console.WriteLine(words);

namespace Users
{
    class User(string name, string occupation)
    {
        public string Name { get; } = name;
        public string Occupation { get; } = occupation;
    }
}

We have a User class which does not override the ToString method; it uses the default one inherited from object. Arrays and collections do not override it either.

Console.WriteLine(u);

The ToString method is called when we pass the object to the Console.WriteLine method.

$ dotnet run
Users.User
System.Object
System.Int32[]
System.Collections.Generic.List`1[System.String]

The output contains the namespaces of the types. The `1 denotes a generic type with one type parameter. To print the elements of a collection, we can use, for instance, string.Join(", ", words).

C# override ToString

In the next example, we override the ToString method.

Program.cs
var u1 = new User("John Doe", "gardener");
var u2 = new User("Roger Roe", "driver");

Console.WriteLine(u1);
Console.WriteLine(u2);
Console.WriteLine($"Users: {u1}, {u2}");

string s = "First user: " + u1;
Console.WriteLine(s);

class User(string name, string occupation)
{
    public string Name { get; } = name;
    public string Occupation { get; } = occupation;

    public override string ToString()
    {
        return $"User {{ {Name} {Occupation} }}";
    }
}

The overridden ToString method returns the user's name and occupation, giving a more meaningful output.

return $"User {{ {Name} {Occupation} }}";

In an interpolated string, literal curly brackets are written as {{ and }}.

Console.WriteLine($"Users: {u1}, {u2}");

string s = "First user: " + u1;

The method is also called by string interpolation and by string concatenation.

$ dotnet run
User { John Doe gardener }
User { Roger Roe driver }
Users: User { John Doe gardener }, User { Roger Roe driver }
First user: User { John Doe gardener }

An overridden ToString should be fast, should not have side effects, should not throw exceptions, and should never return null. Debuggers call it to display objects, too.

C# record ToString

The record type provides a compiler-generated implementation of the ToString method, which includes the names and values of the public properties and fields.

Program.cs
var u1 = new User("John Doe", "gardener");
var u2 = new User("Roger Roe", "driver");

Console.WriteLine(u1);
Console.WriteLine(u2);

record User(string Name, string Occupation);

In this example, we use a record instead of a class.

$ dotnet run
User { Name = John Doe, Occupation = gardener }
User { Name = Roger Roe, Occupation = driver }

C# ToString and null

Calling ToString on a null reference throws a NullReferenceException. The methods that call ToString implicitly handle null values for us.

Program.cs
User? u = null;

Console.WriteLine(u);
Console.WriteLine($"[{u}]");
Console.WriteLine(u?.ToString() ?? "no user");
Console.WriteLine(Convert.ToString(u) == "");

try
{
    Console.WriteLine(u!.ToString());
}
catch (NullReferenceException e)
{
    Console.WriteLine(e.Message);
}

record User(string Name);

The Console.WriteLine method, string interpolation, and Convert.ToString turn null into an empty string. With the null-conditional and null-coalescing operators, we can provide a replacement text.

$ dotnet run

[]
no user
True
Object reference not set to an instance of an object.

C# DateTime ToString

Formatting datetime data is complex; not only do different cultures have different standards for displaying date and time, each culture may have multiple ways of displaying them.

The DateTime type overrides the ToString method and provides overloads that take a format string and a culture.

Program.cs
using System.Globalization;

var d = new DateTime(2026, 5, 20, 14, 35, 12);

Console.WriteLine(d.ToString(CultureInfo.InvariantCulture));
Console.WriteLine(d.ToString(new CultureInfo("en-US")));
Console.WriteLine(d.ToString(new CultureInfo("de-DE")));

Console.WriteLine(d.ToString("MMMM dd, yyyy (dddd)", new CultureInfo("en-US")));
Console.WriteLine(d.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine(d.ToString("o"));

var sk = new CultureInfo("sk-SK");
Console.WriteLine($"Dátum a čas: {d.ToString("F", sk)}");

In the example, we format a datetime value with the default format of several cultures, with custom formats, and with a standard format.

Console.WriteLine(d.ToString(CultureInfo.InvariantCulture));
Console.WriteLine(d.ToString(new CultureInfo("en-US")));
Console.WriteLine(d.ToString(new CultureInfo("de-DE")));

Without a format string, the general date and time pattern of the given culture is used. The parameterless ToString uses the current culture of the thread, so its output depends on the settings of the computer.

Console.WriteLine(d.ToString("MMMM dd, yyyy (dddd)", new CultureInfo("en-US")));
Console.WriteLine(d.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine(d.ToString("o"));

We use two custom formats and the round-trip o standard format, which produces an ISO 8601 string that is the same in all cultures.

Console.WriteLine($"Dátum a čas: {d.ToString("F", sk)}");

The F standard format gives the full date and time pattern; we use it with the Slovak culture.

$ dotnet run
05/20/2026 14:35:12
5/20/2026 2:35:12 PM
20.05.2026 14:35:12
May 20, 2026 (Wednesday)
2026-05-20 14:35:12
2026-05-20T14:35:12.0000000
Dátum a čas: streda 20. mája 2026 14:35:12

See the C# DateTime format article for more details.

C# ToString with monetary data

Formatting monetary data is also complex. Countries use different decimal separators and currency symbols. Also, the currency symbol can be placed in front of or behind the value.

Program.cs
using System.Globalization;

decimal val = 766.45m;

string[] cultures = ["en-US", "fr-FR", "de-CH", "sk-SK", "ja-JP"];

foreach (var name in cultures)
{
    var ci = new CultureInfo(name);
    Console.WriteLine($"{name}: {val.ToString("C2", ci)}");
}

The example uses the C2 currency format specifier with five different cultures. The number after the C is the number of decimal digits.

$ dotnet run
en-US: $766.45
fr-FR: 766,45 €
de-CH: CHF 766.45
sk-SK: 766,45 €
ja-JP: ¥766.45

Note that the culture only determines the format and the symbol; the value is not converted to another currency.

C# overloaded ToString

In the following example, we overload the ToString method.

Program.cs
var t1 = new Temperature(0m);
Console.WriteLine(t1.ToString());
Console.WriteLine(t1.ToString(Scale.Kelvin));
Console.WriteLine(t1.ToString(Scale.Fahrenheit));

Console.WriteLine("--------------------");

var t2 = new Temperature(-20m);
Console.WriteLine(t2.ToString());
Console.WriteLine(t2.ToString(Scale.Kelvin));
Console.WriteLine(t2.ToString(Scale.Fahrenheit));

Console.WriteLine("--------------------");

var t3 = new Temperature(32m);
Console.WriteLine(t3.ToString());
Console.WriteLine(t3.ToString(Scale.Kelvin));
Console.WriteLine(t3.ToString(Scale.Fahrenheit));

enum Scale
{
    Celsius,
    Kelvin,
    Fahrenheit
}

class Temperature(decimal celsius)
{
    public decimal Celsius { get; } = celsius;
    public decimal Kelvin => Celsius + 273.15m;
    public decimal Fahrenheit => Celsius * 9 / 5 + 32;

    public override string ToString()
    {
        return ToString(Scale.Celsius);
    }

    public string ToString(Scale scale)
    {
        return scale switch
        {
            Scale.Celsius => $"{Celsius:N2} °C",
            Scale.Fahrenheit => $"{Fahrenheit:N2} °F",
            Scale.Kelvin => $"{Kelvin:N2} K",
            _ => throw new ArgumentOutOfRangeException(nameof(scale))
        };
    }
}

We have a Temperature type. It can be expressed in Celsius, Fahrenheit, or Kelvin scales. We have an overloaded ToString method which formats the temperature based on the provided scale.

public override string ToString()
{
    return ToString(Scale.Celsius);
}

The overridden method calls the overloaded one, passing it the Scale.Celsius value.

return scale switch
{
    Scale.Celsius => $"{Celsius:N2} °C",
    Scale.Fahrenheit => $"{Fahrenheit:N2} °F",
    Scale.Kelvin => $"{Kelvin:N2} K",
    _ => throw new ArgumentOutOfRangeException(nameof(scale))
};

A switch expression is used to format the temperature value, based on the given scale. The N2 format gives two decimal places. The discard arm handles invalid enumeration values, such as (Scale) 10.

$ dotnet run
0.00 °C
273.15 K
32.00 °F
--------------------
-20.00 °C
253.15 K
-4.00 °F
--------------------
32.00 °C
305.15 K
89.60 °F

C# IFormattable

An overloaded ToString method is not used by string interpolation or by string.Format. To support format strings such as {t:K}, a type implements the IFormattable interface. Its ToString(string? format, IFormatProvider? provider) method receives the format string and the culture. This is how the built-in types, such as int, decimal, or DateTime, support formatting.

Program.cs
using System.Globalization;

var t = new Temperature(21.5m);

Console.WriteLine(t);
Console.WriteLine($"{t:C} | {t:K} | {t:F}");
Console.WriteLine(string.Format("{0:F}", t));
Console.WriteLine(t.ToString("K", new CultureInfo("sk-SK")));

try
{
    Console.WriteLine($"{t:X}");
}
catch (FormatException e)
{
    Console.WriteLine(e.Message);
}

class Temperature(decimal celsius) : IFormattable
{
    public decimal Celsius { get; } = celsius;
    public decimal Kelvin => Celsius + 273.15m;
    public decimal Fahrenheit => Celsius * 9 / 5 + 32;

    public override string ToString() => ToString("C", CultureInfo.CurrentCulture);

    public string ToString(string? format, IFormatProvider? provider)
    {
        format = string.IsNullOrEmpty(format) ? "C" : format;
        provider ??= CultureInfo.CurrentCulture;

        return format.ToUpperInvariant() switch
        {
            "C" => Celsius.ToString("N2", provider) + " °C",
            "F" => Fahrenheit.ToString("N2", provider) + " °F",
            "K" => Kelvin.ToString("N2", provider) + " K",
            _ => throw new FormatException($"The '{format}' format string is not supported.")
        };
    }
}

The Temperature class supports the C, F, and K format strings.

format = string.IsNullOrEmpty(format) ? "C" : format;
provider ??= CultureInfo.CurrentCulture;

Both parameters may be null; for instance, when the object is used in an interpolated string without a format. We fall back to the Celsius scale and the current culture.

"C" => Celsius.ToString("N2", provider) + " °C",

We pass the format provider on to the decimal values, so that they use the decimal separator of the requested culture.

_ => throw new FormatException($"The '{format}' format string is not supported.")

By convention, an unsupported format string results in a FormatException.

$ dotnet run
21.50 °C
21.50 °C | 294.65 K | 70.70 °F
70.70 °F
294,65 K
The 'X' format string is not supported.

The Slovak culture uses a comma as the decimal separator.

Source

Object.ToString method

IFormattable interface

In this article we have worked with the Object.ToString method in C#.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.