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:

See also