How JSON type inference works

Published October 17, 2026

A JSON document carries values but no schema, so a code generator has to guess the types. This guide describes the rules JSON2Struct applies, as the code implements them; other tools choose differently. Every language page runs the same inference and only the final spelling differs, which is why the examples show Java, Go and Python names side by side.

Numbers: int, long or double

A whole number from -2,147,483,648 to 2,147,483,647 becomes an integer, a whole number outside that range becomes a long, and anything with a fractional part becomes a double.

JSON valueInferred asJavaGoPython
42integerIntegerintint
3000000000beyond 32 bitsLongint64int
9.99decimalDoublefloat64float
1.0decimalDoublefloat64float
"2024-05-01T10:30:00Z"timestampLocalDateTimetime.Timedatetime
only ever nullnull typeObjectinterface{}Any

A decimal point or exponent in the written number (10.0, 1e3) makes it a decimal even when the value is whole; a plain 10 stays an integer. A mix such as [1, 2.0] widens to a decimal.

Strings and timestamps

A string is a date-time only if it matches YYYY-MM-DDTHH:mm:ss, optionally followed by exactly three fractional digits and a Z, and it is a real date. Date-only strings such as 2024-05-01, values with an offset such as +02:00, and timestamps with microseconds stay plain strings. Go differs: time.Time cannot decode a zone-less value, so there a Z or an offset like +02:00 is required. Detection is always on; there is no option for it.

Null, empty and missing values

A key that is only ever null has no type to infer, so it takes the "null type" option: Object, interface{} or Any by default, or a string. A null among other values is ignored when types are merged, so [{"x": null}, {"x": 5}] gives x the type of the 5. With "Use Primitives" on, Java keeps the boxed type (Integer) for such a key, and for a key missing from some elements, because a primitive cannot hold null.

An empty array has no elements to learn from and becomes a list of Object. It also wins a merge: given [{"tags": []}, {"tags": ["x"]}], tags is still a list of Object, not a list of strings. There is no notion of an optional field. If a key appears in only some elements, it still becomes a field, with no marker that it can be absent.

Merging array elements

The elements of an array are inferred one by one and then merged into a single type. Numbers widen along integer, long, double, and the widest wins, so [1, 2.5, 3000000000] is a double. A timestamp merged with an ordinary string becomes a string. Two lists merge by merging their element types, so arrays of arrays keep every layer: [[1, 2]] is a list of lists of integers. Anything else that disagrees, such as a number and a string, or a boolean and a number, merges to Object, so [1, "a"] is a list of Object.

Arrays of objects are merged key by key into one class. The class has the union of all keys, in the order they first appear, and each key's type is merged across every element. If one element has "v": 1 and another has "v": "a", the field is Object. A nested object under the same key in several elements is merged the same way.

Naming nested types

A nested class is named after its key in PascalCase: user_profile becomes UserProfile. Nothing is singularized, so an items array of objects produces a class called Items. Field names follow your naming convention, and the original key is kept for the annotation, tag or serializer.

Two keys can produce the same class name. If the field shapes are identical, they share one class. If the shapes differ, the later one is renamed. Every other language prefixes the parent class name, so an address inside customer becomes CustomerAddress. Java uses the initials of the name plus Data, so the same address becomes AData. A numeric suffix is the last resort. Names are compared ignoring case, since each class becomes a file, and Python renames a class like Time that would shadow a standard-library module. A second key converting to the same field name, such as user_name and userName, gets a numeric suffix.

A worked example

{
  "id": 3000000000,
  "ratio": 1.0,
  "readings": [{ "n": 1 }, { "n": 2.5 }],
  "owner": { "name": "Ada" },
  "deleted_at": null,
  "created": "2024-05-01T10:30:00Z"
}

On the JSON to Go converter this produces the following structs, which it writes as one file each. id is int64 because it exceeds 32 bits. ratio is float64 because it is written 1.0. readings merges 1 and 2.5 into float64. deleted_at is interface{}, and created is a time.Time.

type Sample struct {
	Id int64 `json:"id"`
	Ratio float64 `json:"ratio"`
	Readings []Readings `json:"readings"`
	Owner Owner `json:"owner"`
	DeletedAt interface{} `json:"deleted_at"`
	Created time.Time `json:"created"`
}

type Readings struct {
	N float64 `json:"n"`
}

type Owner struct {
	Name string `json:"name"`
}

What one sample cannot tell you

Inference sees only the values you paste. It cannot know that a field is optional, that an integer could be a long in production, that a string is an enum, or that an object with changing keys is really a map. To widen the result, put several payloads in an array under one key, for example {"items": [payload1, payload2]}, and the merge rules above will combine them. Then review the output against the API documentation. Every converter, from JSON to Java to JSON to Scala, applies these rules.

Related guides