vernac / architecture, expressed.
Browse documentation

Try adapter DTOs and PATCH

This tutorial builds on outbound ports. DTOs describe external wire data. They do not change your port’s domain-oriented signature.

Add the JSON runtime to the application POM, using the same Vernac version as your compiler:

<dependency>
    <groupId>org.vernac</groupId>
    <artifactId>vernac-runtime-json</artifactId>
    <version>${vernac.version}</version>
</dependency>

Here vernac.version is an application property containing that version. When your application manages the dependency centrally, use that managed version instead.

Define local transport records

Create src/main/vernac/example/integration/archive.vernac:

namespace example.integration;

port Archive {
    void ping();
}

adapter LocalArchive implements Archive {
    behavior {
        public void ping() {
            // A minimal operation while we explore the DTO contract.
        }
    }

    schema DocumentResponse naming snake_case (
        String documentId,
        list<Line> lines,
        String? comment,
        String? reason absent as null
    );

    schema Line(String productCode, int quantity);

    schema DocumentPatch (
        patch String documentId,
        patch String? comment
    );
}

Run the application’s usual Maven build. Open the generated example.integration.adapter.outbound.dto.LocalArchiveDtos holder. Its nested records are transport objects, separate from the domain Archive interface.

Read a response

In Java, including a test or runner:

import com.fasterxml.jackson.databind.ObjectMapper;
import example.integration.adapter.outbound.dto.LocalArchiveDtos.DocumentResponse;

var json = new ObjectMapper();
var response = json.readValue("""
        {
          "document_id": "doc-7",
          "lines": [{"productCode": "ABC", "quantity": 2, "newField": true}],
          "futureExtension": "ignored"
        }
        """, DocumentResponse.class);

assert response.documentId().equals("doc-7");
assert response.comment().isEmpty();
assert response.lines().getFirst().quantity() == 2;

Unknown fields are ignored even inside Line. Notice that Line did not declare snake_case: its JSON field remains productCode. No convention is silently inherited.

Try removing quantity, supplying null, or replacing 2 with "2". These are errors identifying $.lines[0].quantity, not implicit zero or automatic conversion. Replace lines with an empty array: that is a valid present empty list.

On serialization, absent comment is omitted and absent reason becomes JSON null:

String encoded = json.writeValueAsString(response);
var tree = json.readTree(encoded);
assert !tree.has("comment");
assert tree.get("reason").isNull();

For direct creation use DocumentResponse.of(id, lines, null, null). Factories accept nullable ordinary optional arguments; record accessors return Optional. Lists are copied and cannot be mutated through the DTO.

Express PATCH intent

import example.integration.adapter.outbound.dto.LocalArchiveDtos.DocumentPatch;
import org.vernac.runtime.ports.PatchField;

var update = DocumentPatch.of(PatchField.unchanged(), PatchField.clear());
assert json.writeValueAsString(update).equals("{\"comment\":null}");

var decoded = json.readValue("{}", DocumentPatch.class);
assert decoded.documentId().isUnchanged();
assert decoded.comment().isUnchanged();

Try decoding {"documentId":null}. It fails because the field is not clearable. Try {"comment":""}: this sets an empty string and differs from clearing. Direct Java construction rejects the same illegal clear operation.

Nothing here applies changes to an aggregate or makes a network request. That separation is intentional: DTO conversion preserves protocol intent; application and domain code decide how to act on it.

Use DTOs inside adapter Java

The generated record can be imported in the adapter’s behavior block:

java imports {
    example.integration.adapter.outbound.dto.LocalArchiveDtos.DocumentResponse;
}

Keep it out of the domain port signature. Continue with typed mappings, then generated REST calls. The complete current rules are in the DTO contract.