CodeGym /Courses /Java Server /Provider DTO and normalized DTO

Provider DTO and normalized DTO

Java Server
Level 16, Lesson 4
Available

1. Provider DTO is not an application model

When you write a client for an external API, it is very easy to fall into a psychological trap: once we map JSON into nice Java objects, it feels like those are “our data.” But the external provider did not sign a pact of eternal friendship and stable field names with you. It lives its own life, and its JSON is its language, its habits, and its quirks.

Imagine you came to another country and learned exactly one phrase: “Where is the library?” You can use that phrase, but if you start building your whole life on it (work, relationships, a mortgage), the first change in context will break you. Provider DTO is the “learned phrase,” and what we need is the project’s internal dictionary.

In ReadLater Starter, this is especially important because we have a clear educational boundary: the catalog search command and the catalog details command. Both must work with the result in terms of our project, not in terms of “what the external API happened to call it this morning.”

2. Provider DTO: a mirror of the external contract

A Provider DTO is a model that closely reflects the provider’s JSON: its field names, its structure, its wrappers, and its nesting. Provider DTOs do not have to be pretty. Moreover, if you start “renaming everything to fit your style” right at this level, you lose an important thing: the ability to easily compare the DTO with sample JSON and the provider documentation.

Let’s take a typical search response example: the provider has an array called docs, and inside each item the fields may be named as if they were chosen by a vote in a chat of three people. (Joke. Usually it is chosen by one person and a deadline.)

Example of a provider DTO for a search item:

package com.example.readlater.catalog.dto.provider;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;

/**
 * Provider DTO: a model as close as possible to the provider's fields.
 * Here we intentionally stay in the terms of the external contract.
 *
 * key — the external book identifier in the provider's catalog
 * title — the title as sent by the provider
 * authorNames — a list of authors from the author_name field (that is what it is called in JSON)
 */
public record OpenLibraryDocDto(
    String key,
    String title,
    @JsonProperty("author_name") List<String> authorNames
) {}

There are a few important ideas here, even though the code is short.

First, we do not pretend that author_name is a “wrong name.” It is the provider’s name, and we accept it. Second, we still want a convenient Java name (authorNames), so we use @JsonProperty, and Jackson understands that it is the same field.

Now the response wrapper:

package com.example.readlater.catalog.dto.provider;

import java.util.List;

/**
 * Provider DTO: search response wrapper from the provider.
 * The fields and names are exactly as in the contract (docs, numFound), without "our" renaming.
 */
public record OpenLibrarySearchResponseDto(
    List<OpenLibraryDocDto> docs,
    int numFound
) {}

Yes, numFound may or may not be needed — but for now we have described it honestly, because it really exists in the contract. And most importantly: at this level we are still living in the provider’s world. The names docs, key, and author_name are all external.

3. Normalized DTO: the internal language of the project

A Normalized DTO is a model that reflects our scenarios, not someone else’s JSON structure. Here we choose convenient names, convenient types, and a convenient data shape. Normalized DTOs are what application teams (catalog search, catalog details) and any client-side business logic should operate on.

For example, in catalog search we definitely need an “external book identifier,” a “title,” and “one author for a nice display” (even if the provider returns a list of authors). The internal search model can be simple:

package com.example.readlater.catalog.dto.normalized;

/**
 * Normalized DTO: an internal model of the search result for our application.
 * This is already "our language", independent of the provider's field names.
 */
public record CatalogBookSearchItem(
    String externalId,
    String title,
    String author
) {}

Pay attention to the two renamings.

The provider field key became externalId. In our project, that is clearer: it is the ID in the external catalog. The field author_name (a list) turned into author (a string). This is already a normalization decision: for console output, we take one author.

And that is normal. Normalized DTOs exist precisely so that we can make a decision about what shape of data we want inside the project.

4. Mapping-step: normalization as a separate step

When you first learn about provider DTOs and normalized DTOs, there is a temptation to do the mapping “along the way”: an if here, a substring there, a stream() somewhere else, and somehow it all works out. But that quickly turns into a mess. It is much easier for your head — and for maintenance — if there is an explicit normalization step between these models: a separate method or class responsible for the transformation.

A very simple diagram helps to keep this in sight:

flowchart TD
    A["raw JSON string"] --> B["Provider DTO OpenLibrarySearchResponseDto"]
    B --> C["Normalizer / Mapper"]
    C --> D["Normalized DTO CatalogBookSearchItem"]
    D --> E["Rest of the application code for catalog search / details"]

This diagram matters because “the dirt of the external world” (unstable fields, strange names, extra data) stops at stage B→C. After that, at point D, the code starts living with normal names and a predictable shape.

Example of normalizing one item:

package com.example.readlater.catalog.service;

import com.example.readlater.catalog.dto.normalized.CatalogBookSearchItem;
import com.example.readlater.catalog.dto.provider.OpenLibraryDocDto;

public class CatalogNormalizer {

    public static CatalogBookSearchItem toSearchItem(OpenLibraryDocDto doc) {
        // The external world may send null or an empty list — that is normal for JSON.
        // Our job is to bring it into a stable shape for the rest of the application code.
        String author = (doc.authorNames() == null || doc.authorNames().isEmpty())
            ? "Unknown author" // Default so the UI/CLI does not crash on empty data
            : doc.authorNames().get(0); // For search, take the first author (normalization decision)

        // Convert the provider's key into an external ID in our project's terms.
        return new CatalogBookSearchItem(doc.key(), doc.title(), author);
    }
}

Yes, there is a small “business decision” here: if there are no authors, we write Unknown author. You could choose different behavior, but the important thing is that it is now concentrated in one place. And if you decide to replace "Unknown author" with "—", you change one line instead of searching through the project for 17 places where someone “temporarily” inserted a default.

And now the full path: JSON → provider response → list of normalized items.

import com.example.readlater.catalog.dto.normalized.CatalogBookSearchItem;
import com.example.readlater.catalog.dto.provider.OpenLibrarySearchResponseDto;
import com.example.readlater.catalog.service.CatalogNormalizer;
import tools.jackson.databind.ObjectMapper;
import java.util.List;

public class CatalogSearchParser {
    // ObjectMapper — the place where we turn a "raw" JSON into a provider DTO.
    private final ObjectMapper mapper = new ObjectMapper();

    public List<CatalogBookSearchItem> parse(String json) throws Exception {
        // 1) Deserialize strictly into the provider DTO (in the provider's terms).
        OpenLibrarySearchResponseDto dto = mapper.readValue(json, OpenLibrarySearchResponseDto.class);

        // 2) Normalize into internal DTOs that are safe to pass to the rest of the code.
        return dto.docs().stream().map(CatalogNormalizer::toSearchItem).toList();
    }
}

Notice that we are not showing the network call here — we already have that in the transport layer. Here we only show the piece “after response.body().” And that makes sense: the JSON mapping layer sits between transport and the rest of the application.

5. Preventing provider DTO leakage

The most common reason provider DTOs start living everywhere is very simple: you just return them from the client. And then someone “outside” starts using them because “well, they are already there.”

A strict rule helps here: the outer layer (the client to the provider) should return only normalized DTOs. In other words, method signatures are your border guard. If at the boundary you return ProviderBookDto, you have opened the gate yourself.

Let’s compare two signatures. The first one is bad because it leaks the external contract:

import com.example.readlater.catalog.dto.provider.OpenLibraryDocDto;
import java.util.List;

public interface CatalogClientBadIdea {
    // Bad idea: a provider DTO leaks out, and the rest of the code will start living by the provider's contract.
    List<OpenLibraryDocDto> search(String query);
}

The second one is good because the internal language of the project comes out:

import com.example.readlater.catalog.dto.normalized.CatalogBookSearchItem;
import java.util.List;

public interface CatalogClient {
    // Good idea: return only normalized DTOs to the outside world (the project's internal language).
    List<CatalogBookSearchItem> search(String query);
}

The difference seems to be “only in types.” But in practice, it is the difference between a project that survives provider changes and a project that gets fixed like an old TV: “hit it from the side — it works, but better not touch it.”

To reinforce the idea, let’s put the comparison into a table. This is not theory for theory’s sake, but literally “what you will see in the code in a week.”

Characteristic Provider DTO Normalized DTO
Who owns the model The provider Our project
Field names author_name, docs, key externalId, items, author
Stability Can change without warning Stable until we decide to change it
Where it lives Inside catalog client In the application layer catalog search/details
What to do when the provider changes Change the DTO and the normalizer Ideally — leave the rest of the code alone

And yes: provider DTOs can even be non-public. For example, you can keep them in a package that nobody “accidentally” imports. But even without that, if you have designed your signatures correctly, they still will not leak out.

6. Normalization: a place for small decisions

Many beginners are afraid of the idea of “normalization”: it feels as if we are “distorting the data.” In reality, we are not distorting it, but bringing data into a form convenient for scenarios. In the real backend world, this is standard practice: the external contract is almost always richer, stranger, and less convenient than what your application needs.

For example, the provider may give a list of authors, while for catalog search we need one author to display the result nicely in the console. That is a normalization decision: take the first author or join them into a string with commas. The provider may return an empty list, but we need something to show the user — we set a default. The provider may return title with extra spaces — we apply trim().

It is better to keep such decisions in one place — in the normalizer. And it is highly desirable that they be as simple and clear as possible. A normalizer is not the place to “launch a mini AI to guess who the main author is.” Do not turn it into philosophy.

Example of a slightly more “careful” author choice without overcomplicating things:

import java.util.List;

public class AuthorPicker {

    public static String pickSingleAuthor(List<String> authors) {
        // If there are no authors, return a default so the rest of the code does not do null checks everywhere.
        if (authors == null || authors.isEmpty()) {
            return "Unknown author";
        }

        // Take the first author and trim spaces: this is a typical small normalization step.
        return authors.get(0).trim();
    }
}

This code is primitive, but in a learning project that is a plus. It is easy to read, easy to change, and hard to break.

7. Scenario: the provider changed the contract

Imagine a typical day in the life of an external API: they “slightly improved the contract” (read: renamed a field), and now instead of author_name you get authors. If provider DTO and normalized DTO are merged into one, you change the DTO — and then, in a cascade, all the places where you called authorNames() break.

If, however, you have a clear boundary, the area of change becomes small: the provider DTO (and possibly one @JsonProperty annotation) plus the normalizer. The normalized models and the rest of the code do not know and should not know what is happening on the provider’s side.

For example, you only change the provider DTO:

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;

/**
 * Provider DTO after a provider contract change.
 * We adapt at the boundary so the internal project code does not change.
 */
public record OpenLibraryDocDto(
    String key,
    String title,
    @JsonProperty("authors") List<String> authorNames // We kept the Java name the same; only the JSON binding changed
) {}

And the normalizer that works with authorNames() may not even need to change. Yes, this is a small example, but it shows the main point: we localize the chaos of the external world.

In industrial development, this is sometimes called an anti-corruption layer (anti-corruption layer): a layer that prevents the external system from “polluting” the internal model. The name sounds formidable, but the meaning is simple: “do not bring other people’s quirks into your house.”

8. Typical mistakes when working with DTOs

Mistake #1: “Why two DTOs? I will make one and just name it nicely.”
This kind of solution usually looks like record BookDto(String key, @JsonProperty("author_name") List<String> author, ...), and then this DTO is used both as a provider DTO and as an internal one. The problem does not show up immediately, but when you need to rename key to externalId in the application logic, or when the same DTO starts serving both search and details, even though the response shapes are different. In the end, the DTO bloats, becomes “for everything,” and you lose the clarity of boundaries.

Mistake #2: normalization is scattered around the project “along the way.”
Today you wrote doc.authorNames().get(0) somewhere, tomorrow in another place String.join(", ", doc.authorNames()), and the day after that somewhere else you put "Unknown author". Externally everything works, but the behavior becomes unpredictable: one screen shows one author, another shows all of them, and a third shows a default. The normalizer is needed precisely so the behavior is consistent.

Mistake #3: the normalized DTO becomes a copy of the provider DTO “just in case.”
Sometimes a student thinks: “I’ll make a normalized DTO, but I’ll put all the provider’s fields in it, just in case they come in handy.” That is exactly the moment when the normalized DTO stops being the internal language of the project and becomes a mirror of the external world again, just under a different name. A normalized model should contain exactly what your current scenarios need; otherwise, you lose the point of separation.

Mistake #4: provider field names seep into the normalized DTO.
If your project model ends up with fields like doc, numFound, key, and author_name, then you have effectively declared the provider to be the “main owner of meaning.” Internal models should speak your language: externalId, items, count, author. This is not aesthetics; it is a way to keep code readable and independent.

Mistake #5: no handling of null/empty lists in the normalizer.
In real external JSON, “sometimes there is no author,” “sometimes the list is empty,” “sometimes the field is missing.” If the normalizer calls get(0) without checking, you will get a lovely IndexOutOfBoundsException at the worst possible time — usually when you are demonstrating the project to someone else. The normalizer is the place where you need to “land” the data into a stable shape.

1
Task
Java Server, level 16, lesson 4
Locked
One provider DTO and one normalized DTO
One provider DTO and one normalized DTO
1
Task
Java Server, level 16, lesson 4
Locked
List of normalized DTOs from provider search response
List of normalized DTOs from provider search response
1
Survey/quiz
HTML file:, level 16, lesson 4
Unavailable
HTML file:
JSON Mapping
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION