DevKitHub

Programming

JSON to Java POJO and Record Converter

Paste a JSON response to get Java classes with getters and setters, or records, with a @JsonProperty or @SerializedName annotation on every field. Every element of an array is merged into one class, so fields that are sometimes null or missing come out boxed.

12 lines
167 lines

Deserialize

Root type
Root
File
Root.java
  • $.placed_at: every sample is an ISO 8601 date-time with a Z or an offset, so the type is OffsetDateTime. Jackson 2 needs the JavaTimeModule from jackson-datatype-jsr310 registered to read it, and SerializationFeature.WRITE_DATES_AS_TIMESTAMPS disabled to write it back as text; Jackson 3 and Spring Boot do both already. Jackson converts the offset to UTC as it reads unless DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE is disabled.
  • $.delivery_date: every sample is a date without a time, so the type is LocalDate. Jackson 2 needs the JavaTimeModule from jackson-datatype-jsr310 registered to read it, and SerializationFeature.WRITE_DATES_AS_TIMESTAMPS disabled to write it back as text; Jackson 3 and Spring Boot do both already.

The classes describe the sample you paste: paste an array of several responses so that fields which are sometimes missing or null come out boxed. Classes need Java 8 or later, records Java 16. With separate files, save each section under the name in its first line.

This tool runs entirely in your browser. Your input is never uploaded, stored or logged.

How it works

The samples are merged before any Java is written: every value found at the same position counts, so an array of orders gives one class typed from all of them, not from the first. A field present and non-null in every sample is a primitive, long, double or boolean; one that is null in some sample or missing from some object is boxed, as Long, Double or Boolean, because a primitive cannot hold null: Jackson 2 quietly reads null as 0, so a real 0 can no longer be told from no value, and Jackson 3 refuses it. Integers are long rather than int, as a sample says nothing about the largest ID to come, and an integer beyond long is BigInteger. Anything written with a fraction or an exponent is double, or BigDecimal if you choose it, which keeps 19.99 and even the trailing zero of 1.50 exactly; a number that a double would round, or cannot hold at all, is BigDecimal whatever you choose.

A string field is OffsetDateTime only when every sample is an ISO 8601 date-time with seconds and a Z or an offset within ±18:00, LocalDate when every sample is a date, and LocalDateTime when every sample is a date-time with no zone; anything else, including "", stays String. Jackson 2 reads these types only once the JavaTimeModule from jackson-datatype-jsr310 is registered, and even then writes an OffsetDateTime as the number 1790569800.000000000 and a LocalDate as the array [2026,9,28] until SerializationFeature.WRITE_DATES_AS_TIMESTAMPS is disabled; Jackson 3 and Spring Boot’s ObjectMapper do both already. Jackson also moves an OffsetDateTime to UTC as it reads, so +05:30 comes back as Z, the same instant, unless ADJUST_DATES_TO_CONTEXT_TIME_ZONE is disabled. Gson has no adapters for java.time, so with Gson dates stay String.

Field names are lowerCamelCase, with the key kept in the annotation: user_id, user-id, userId and USER_ID all give userId, a keyword gets an underscore (class_), and keys that give the same name are numbered. Getters follow the rule Jackson reads a name back with. A field eTag gets geteTag(), because Jackson reads getETag() as a second property, etag in Jackson 2 and ETag in Jackson 3, and writes the value twice; a boolean isActive gets getIsActive(), not the isActive() an IDE or Lombok writes, which Jackson reads as active. A key missing from some samples gets @JsonInclude(NON_NULL), so Jackson leaves it out on the way back instead of sending null; Gson leaves out every null unless serializeNulls() is set. Records have no getters, but a component may not be called hashCode or toString, so those get an underscore too. Nested objects become static nested classes of the first class, shared when identical, or separate files.

Common problems

Every example below is run against this tool in our test suite, so what it says here is what the tool actually does.

Object keys must be strings in double quotes, as in {"name": 1}.

{id=1, name=Ada, tags=[a, b]}
Why:
The output of Map.toString(), or a logger printing a Map, which Java writes as {key=value} without quotes. It looks like JSON in a log, but it is not, and a string such as "a, b" cannot be told apart from a list.
Fix:
Log the JSON itself, objectMapper.writeValueAsString(map) or gson.toJson(map), and paste that.

Expected "," or "}" after a value in an object.

{"id": 1L, "price": 9.99f}
Why:
Java literals copied from a test or a builder. 1L, 9.99f, 0x1F and 1_000 are Java; JSON has one kind of number, with no suffixes, underscores or hexadecimal.
Fix:
Write the numbers plainly, 1 and 9.99; the generator chooses long, double or BigDecimal from how they are written.

UnrecognizedPropertyException: Unrecognized field "x" (class Root), not marked as ignorable

Why:
A real response has a key that none of the samples had. Jackson 2 fails on any unknown property by default; Jackson 3 and Spring Boot ignore them, and Gson always does.
Fix:
Turn on Ignore unknown, which adds @JsonIgnoreProperties(ignoreUnknown = true), or disable FAIL_ON_UNKNOWN_PROPERTIES on the ObjectMapper.

Java 8 date/time type `java.time.OffsetDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling

Why:
The class has an OffsetDateTime or LocalDate field and the ObjectMapper is plain Jackson 2, which reads java.time types only through the JavaTimeModule.
Fix:
Add jackson-datatype-jsr310, register new JavaTimeModule() and disable WRITE_DATES_AS_TIMESTAMPS; or turn off java.time to keep the fields String.

A price of 19.99 arrives as 19.

Why:
Every sample of the field was a whole number, so it is a long, and the real API also sends fractions. Jackson truncates 19.99 to 19 without an error, because ACCEPT_FLOAT_AS_INT is on by default; Gson refuses it with "Expected a long but was 19.99".
Fix:
Paste a sample with a fractional value, or change the field to double or BigDecimal. Disabling ACCEPT_FLOAT_AS_INT turns the silent truncation into an error.

Frequently asked questions

Should I use Jackson or Gson?
Jackson is what Spring Boot and most server frameworks use, and it reads java.time types and records. Gson is common on Android and in older code; it reads and writes fields directly, ignoring getters, and ignores unknown keys, but it has no java.time support and by default accepts text that is not JSON, such as comments and single quotes. The generated classes differ only in the annotations and imports.
Which Java version does the output need?
Classes compile on Java 8 and later; records need Java 16. Jackson reads records from version 2.12 and Gson from 2.10. Jackson 3 needs Java 17.
How do I read a JSON array into a List?
Pass the element type through a type token, because List<RootItem>.class does not exist: mapper.readValue(json, new TypeReference<List<RootItem>>() {}) with Jackson, or gson.fromJson(json, new TypeToken<List<RootItem>>() {}.getType()) with Gson.
Why are some fields Long instead of long?
Because the samples show them null, or missing from some objects. A long cannot be null, so Jackson 2 and Gson leave 0 there and 0 then means both zero and no value, while Jackson 3 refuses the null. A Long is null for no value; unboxing a null Long, in arithmetic for example, throws a NullPointerException, so check it first.

Last updated