
# 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

```dart
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);
}
```

```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:

```dart
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. |

```dart
// 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":"ada@example.com"}
```

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`.

```dart
@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.

```dart
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`:

```dart
// 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:

```dart
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.*`):

```properties
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:

```dart
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)

```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)` becomes `readValue<Book>(json)`; a generic type is a `GenericType` instead of a `TypeReference`.
- `@JsonProperty` has no `required`; the constructor decides.
- The properties are `spring.json.*` instead of `spring.jackson.*`.
- Only fixed offsets as time zones; there is no time zone database.

## See also

- [Type conversion](../conversion/index.md)
- [Configuration properties](../config/configuration-properties.md)
- [Validation](../validation/index.md)
- [Beans](../beans/index.md)
- [SpEL](../spel/index.md)
