JSON
ObjectMapper turns Dart objects into JSON and JSON back into objects, without a fromJson or toJson per class.
Why
Dart has no runtime reflection in Flutter and AOT builds. Turning a class into JSON usually takes hand-written or generated fromJson / toJson code for every class. The mapper instead reads metadata that spring_core_dart_generator writes for classes marked @Introspected(). It works in AOT, Flutter, the web and minified builds.
The web and Feign starters use the same mapper for request and response bodies, so a controller or a client only declares the model types. The annotations on a model shape its HTTP JSON as well. Use the mapper yourself wherever JSON crosses a boundary: files, caches, message queues.
Example
import 'package:spring_core_dart/spring_core_dart.dart';
@Introspected()
class Book {
const Book({
required this.isbn,
required this.title,
required this.publishedAt,
this.tags = const [],
});
final String isbn;
final String title;
final DateTime publishedAt;
final List<String> tags;
}
@Service()
class Shelf {
// In an application the mapper is a bean, set up from spring.json.*.
Shelf(this.mapper);
final ObjectMapper mapper;
String save(List<Book> books) => mapper.writeValueAsString(books);
List<Book> load(String json) => mapper.readList<Book>(json);
}
[{"isbn":"1","publishedAt":"1965-08-01T00:00:00.000Z","tags":["sf"],"title":"Dune"}]
Properties are written in alphabetical order by default, like Jackson 3. Immutable classes are created through their constructor. A class with setters and a no-argument constructor works too.
A missing property is an error when its constructor parameter is neither nullable nor has a default value. A nullable one reads as null; one with a default keeps it.
Reading and writing
| Method | Does |
|---|---|
writeValueAsString(value) |
An object to JSON text. |
readValue<T>(json, [type]) |
JSON text to a T. |
readList<E>(json), readMap<V>(json) |
JSON text to a List<E> or a Map<String, V>. |
readTree(json) |
JSON text to a JsonNode tree. |
valueToTree(value), treeToValue<T>(tree) |
An object to plain JSON values and back. |
convertValue<T>(value) |
One object to another type through JSON. |
writerWithView(View), readerWithView(View) |
Write or read only the properties of a @JsonView. |
Generic classes are read with their type arguments:
final page = mapper.readValue(
json,
GenericType<Page<Book>>([ClassType<Book>()]),
);
A failure throws a JsonMappingException. Its path names the place, such as items[2].price: expected a number, got "abc". The web starter turns it into a 400 response for request bodies.
Annotations
The annotations follow Jackson's names.
| Annotation | On | Does |
|---|---|---|
@JsonProperty('name') |
property, parameter, enum constant | Another JSON name. access: makes it readOnly or writeOnly. |
@JsonAlias([...]) |
property | Other names it is read from. |
@JsonIgnore() |
property | Left out in both directions. |
@JsonIgnoreProperties([...], ignoreUnknown:) |
class | Drops properties by JSON name; accepts unknown properties. |
@JsonInclude(JsonInclusion.nonNull) |
class, property | Which values are written: always, nonNull, nonAbsent, nonEmpty, nonDefault. |
@JsonPropertyOrder([...]) |
class | Listed properties first. |
@JsonNaming(PropertyNamingStrategy.snakeCase) |
class | Naming strategy of one class. |
@JsonCreator() |
constructor | The constructor used when reading. |
@JsonValue() |
field, getter, method | One value stands for the whole object or enum constant. |
@JsonFormat(pattern:, timezone:, shape:) |
property, class | Date pattern and offset, or another shape. |
@JsonUnwrapped(prefix:, suffix:) |
property | A nested object written inline. |
@JsonAnyGetter(), @JsonAnySetter() |
map property, method | Unknown properties kept in a map and written back. |
@JsonView([...]) |
property, class | Puts a property into views. |
@JsonTypeInfo, @JsonSubTypes, @JsonTypeName |
class | Polymorphic hierarchies with a type id. |
// A REST model: a snake_case date, a password that is read but never written.
@Introspected()
@JsonInclude(JsonInclusion.nonNull)
class Account {
const Account(this.email, this.createdAt, {this.password, this.note});
@JsonAlias(['mail'])
final String email;
@JsonProperty('created_at')
final DateTime createdAt;
@JsonProperty('', access: JsonPropertyAccess.writeOnly)
final String? password;
final String? note;
}
// {"created_at":"2026-01-01T00:00:00.000Z","email":"[email protected]"}
There is no required flag: whether a property must be present comes from the constructor.
Dates and durations
A DateTime is ISO-8601, as Jackson writes an OffsetDateTime: 2026-01-01T12:00:00.000+02:00 for a local time, Z for UTC. Text without an offset is read in the mapper's time zone, else UTC. A Duration is ISO-8601, such as PT1M30S, and is also read from 90s.
@JsonFormat(pattern: 'dd.MM.yyyy', timezone: '+01:00')
final DateTime due; // "24.12.2026"
The pattern takes the letters of Java's DateTimeFormatter. Time zones are UTC, Z or a fixed offset such as +02:00. Named zones such as Europe/Berlin throw, as Dart has no time zone database. With writeDatesAsTimestamps dates are numbers: seconds with a fraction by default, like Jackson 3.
Enums
An enum is written by name and read by name or by index.
enum Stock {
inStock('in_stock'),
soldOut('sold_out');
const Stock(this.json);
@JsonValue()
final String json; // Stock.inStock <-> "in_stock"
}
enum Color {
@JsonProperty('RED')
red,
green, // "green"
}
@JsonFormat(shape: JsonFormatShape.number) on a property writes the index instead.
Types the mapper does not know
Built in: String, int, double, num, bool, DateTime, Duration, Uri, BigInt (as a string), enums, List, Set, Map, nullable types, JsonNode trees and introspected classes. A class with its own fromJson / toJson (from json_serializable or freezed) is converted with those.
For any other type, write a JsonSerializer and a JsonDeserializer:
// Money is written as one string, "1250 EUR".
final class MoneySerializer extends JsonSerializer<Money> {
const MoneySerializer();
@override
Object? serialize(Money value, ObjectMapper mapper) =>
'${value.amount} ${value.currency}';
}
final class MoneyDeserializer extends JsonDeserializer<Money> {
const MoneyDeserializer();
@override
Money deserialize(Object json, ObjectMapper mapper) {
final [amount, currency] = (json as String).split(' ');
return Money(int.parse(amount), currency);
}
}
@Configuration()
class JsonSetup {
@Bean()
JsonModule moneyModule() => const JsonModule(
serializers: [MoneySerializer()],
deserializers: [MoneyDeserializer()],
);
}
In an application every JsonSerializer, JsonDeserializer and JsonModule bean is added to the mapper. Without the container, pass them to ObjectMapper(serializers:, deserializers:, modules:).
JSON without a class
JsonNode holds a document as a tree, for JSON with no fixed shape:
final root = mapper.readTree('{"book": {"title": "Dune", "tags": ["sf"]}}');
root.get('book')?.get('title')?.asString(); // Dune
root.path('book').path('price').asInt(10); // 10 (missing, so the default)
root.at('/book/tags/0').stringValue(); // sf
final body = ObjectNode()
..put('query', 'dune')
..putArray('tags').add('sf');
Configuration
In an application the ObjectMapper bean is set up from spring.json.* (Spring Boot: spring.jackson.*):
spring.json.property-naming-strategy=SNAKE_CASE
spring.json.default-property-inclusion=non_null
spring.json.time-zone=UTC
spring.json.serialization.indent-output=true
spring.json.deserialization.fail-on-unknown-properties=true
spring.json.mapper.sort-properties-alphabetically=false
| Property | Default |
|---|---|
spring.json.property-naming-strategy |
LOWER_CAMEL_CASE |
spring.json.default-property-inclusion |
always |
spring.json.time-zone |
none (UTC) |
spring.json.serialization.indent-output |
false |
spring.json.serialization.write-dates-as-timestamps |
false |
spring.json.serialization.write-durations-as-timestamps |
false |
spring.json.deserialization.fail-on-unknown-properties |
false |
spring.json.deserialization.accept-float-as-int |
true |
spring.json.deserialization.fail-on-null-for-primitives |
true |
spring.json.mapper.sort-properties-alphabetically |
true |
spring.json.mapper.default-view-inclusion |
false |
Without the container, build a mapper in code:
final mapper = JsonMapper.builder()
.propertyNamingStrategy(PropertyNamingStrategy.snakeCase)
.enable(DeserializationFeature.failOnUnknownProperties)
.build();
Inject the bean instead of creating a mapper in application code, so every part of the application writes the same JSON. A JsonMapperBuilderCustomizer bean changes one setting of it; your own ObjectMapper bean replaces it.
Which classes need @Introspected
A class that is converted needs metadata. @Introspected() describes all of it. Classes of the same package that it uses (property and constructor types) are described too. A class the build finds in use without the annotation, such as a controller's return type, a request body or a Feign client's answer, is described for that use only. Classes of other packages are listed with @Introspected(classes: [ExternalDto]).
In Spring (Java)
JsonMapper mapper = JsonMapper.builder().build();
String json = mapper.writeValueAsString(book);
Book again = mapper.readValue(json, Book.class);
List<Book> books = mapper.readValue(json, new TypeReference<List<Book>>() {});
The names and defaults follow Jackson 3 and Spring Boot. Differences that matter:
- Classes need
@Introspected()or must be found in use by the build; Jackson uses reflection. readValue(json, Book.class)becomesreadValue<Book>(json); a generic type is aGenericTypeinstead of aTypeReference.@JsonPropertyhas norequired; the constructor decides.- The properties are
spring.json.*instead ofspring.jackson.*. - Only fixed offsets as time zones; there is no time zone database.