Skip to content

State DTOs

Overview

Maintaining Data Transfer Objects for your contract states can be a mundane, error-prone task. Vaultaire’s annotation processing automates this by (re)generating those DTOs for you.

DTOs generation may require a VaultaireStateUtils annotation to be present for the target model/state.

Usage Patterns

A typical use for generated DTOs is messaging over HTTP REST or RPC as input and output of Corda Flows. The provided conversion utilities can be used to create, update or even patch ContractState types they correspond to.

DTO to State

To convert from DTO to state, use the DTO’s toTargetType() method:

// Using default strategy
// ----------------------
// Get the DTO
val dto1: BookStateClientDto = //...
// Convert to State
val state1: BookState = dto1.toTargetType()

// Using the default strategy
// ----------------------
// Get the Service
val stateService: BookStateService = //...
// Get the 'client' DTO
val dto2: BookStateClientDto = // ...
// Convert to State
val state2: BookState = dto2.toTargetType(stateService)

DTO as Patch Update

DTOs can be used to transfer and apply a “patch” to update an existing state:

// Get the Service
val stateService = BookStateService(serviceHub_or_RPCOps)

// Load state from Node Vault
val state: BookState = stateService.getByLinearId(id)

// Apply DTO as patch
// ----------------------
val patchedState1: BookState = dto1.toPatched(state)

// Apply 'client' DTO as patch
// ----------------------
val patchedState2: BookState = dto2.toPatched(state, stateService)

State to DTO

To convert from state to DTO, use the DTO’s latter’s alternative, state-based constructor:

// Get the state
val state: BookState = stateService.getByLinearId(id)
// Convert to DTO
val dto = BookStateClientDto.from(state)

DTO Generation

This section explains the annotations and strategies involved in generating DTOs.

Annotations

Local States

To have Vaultaire generate DTOs for ContractStates within local (Gradle module) sources, annotate them with @VaultaireStateDto:

@VaultaireStateDto(
    // optional: properties to ignore
    ignoreProperties = ["foo"],
    // optional, default is false
    includeParticipants = false
)
data class BookState(
    val publisher: Party,
    val author: Party,
    val price: BigDecimal,
    val genre: Genre,
    @DefaultValue("1")
    val editions: Int = 1,
    val foo: String = "foo1",
    val title: String = "Uknown",
    val published: Date = Date(),
    @field:JsonProperty("alias")
    val alternativeTitle: String? = null,
    override val linearId: UniqueIdentifier = UniqueIdentifier()
) : LinearState, QueryableState {/* ... */}

Dependency States

To generate DTOs for ContractStates outside the sources in context, e.g. from a contract states module or a project dependency, create a “mixin” class as a placeholder and annotate it with @VaultaireStateDtoMixin.

This approach might be preferred or necessary even for state sources in context or under your control, e.g. when having (the good practice of) separate cordapp modules for contracts/states and flows.

Mixin example:

@VaultaireStateDtoMixin(
    persistentStateType = PersistentBookState::class,
    contractStateType = BookState::class,
    // optional: properties to ignore
    ignoreProperties = ["foo"]
)
class BookStateMixin // just a placeholder for our annotation

Non-State Models

@VaultaireModelDto and @VaultaireModelDtoMixin can be used for generation of DTOs for regular, non-ContractState data classes. to similarly generate REST-friendly DTOs and conversion utils focused op Corda-related types like accounts etc.

@VaultaireModelDtoMixin(baseType = SomeModel::class)
data class SomeModelMixin

Views

Some times using DTOs with a subset of the original state or model’s members is desired. Vaultaire allows automating generation for those DTOs as well using the views property of the annotation in context.

For example, to generate a MagazineStateClientDto as a DTO for MagazineState along with a couple of DTO Views like UpdatePartiesView and AddIssueView that include onle with only (issues, published) and (issues, published) respectively:

@VaultaireStateUtilsMixin(name = "magazineConditions",
        persistentStateType = PersistentMagazineState::class,
        contractStateType = MagazineState::class)
@VaultaireStateDtoMixin(
        persistentStateType = PersistentMagazineState::class,
        contractStateType = MagazineState::class,
        strategies = [VaultaireDtoStrategyKeys.CORDAPP_LOCAL_DTO, VaultaireDtoStrategyKeys.CORDAPP_CLIENT_DTO],
        views = [
            VaultaireView(name = "UpdatePartiesView", viewFields = [
                VaultaireViewField(name = "author"),
                VaultaireViewField(name = "publisher")]),
            VaultaireView(name = "AddIssueView", includeNamedFields = ["issues", "published"])]
)
data class MagazineMixin(
        @DefaultValue("1")
        var issues: Int,
        @DefaultValue("Date()")
        val published: Date,
        @DefaultValue("UniqueIdentifier()")
        val linearId: UniqueIdentifier,
        val customMixinField: Map<String, String> = emptyMap()
)

The views property is available in @VaultaireStateDto, @VaultaireStateDtoMixin, @VaultaireModelDto and @VaultaireModelDtoMixin.

Utility Annotations

The@DefaultValue can be used to provide default property initializers. It can be used equally on either ContractState or “mixin” properties:

@VaultaireStateUtilsMixin(/*...*/)
@VaultaireStateDtoMixin(/*...*/)
data class MagazineMixin(
        @DefaultValue("1")
        var issues: Int,
        @DefaultValue("Date()")
        val published: Date,
        @DefaultValue("UniqueIdentifier()")
        val linearId: UniqueIdentifier
)

Sample DTO

Sample (state) client DTO: nullable var members and utilities to convert from/to
or patch an instance of the target type:

/**
 * A [MagazineContract.MagazineState]-specific
 * [com.github.manosbatsis.vaultaire.dto.VaultaireStateClientDto] implementation
 */
@CordaSerializable
data class MagazineStateClientDto(
        var publisher: AccountInfoStateClientDto? = null,
        var author: AccountInfoStateClientDto? = null,
        var price: BigDecimal? = null,
        var genre: MagazineContract.MagazineGenre? = null,
        var issues: Int? = 1,
        var title: String? = null,
        var published: Date? = Date(),
        var linearId: UniqueIdentifier? = UniqueIdentifier(),
        var customMixinField: Map<String, String>? = null
) : VaultaireAccountsAwareStateClientDto<MagazineContract.MagazineState> {
    /**
     * Create a patched copy of the given [MagazineContract.MagazineState] instance,
     * updated using this DTO's non-null properties.
     */
    @Suspendable
    override fun toPatched(original: MagazineContract.MagazineState,
                           stateService: AccountsAwareStateService<MagazineContract.MagazineState>):
            MagazineContract.MagazineState {
        val publisherResolved = stateService.toAccountPartyOrNull(this.publisher,
                original.publisher)
        val authorResolved = stateService.toAccountParty(this.author, original.author)
        val patched = original.copy(
                publisher = publisherResolved,
                author = authorResolved,
                price = this.price ?: original.price,
                genre = this.genre ?: original.genre,
                issues = this.issues ?: original.issues,
                title = this.title ?: original.title,
                published = this.published ?: original.published,
                linearId = this.linearId ?: original.linearId
        )
        return patched
    }

    /**
     * Create an instance of [MagazineContract.MagazineState], using this DTO's properties.
     * May throw a [DtoInsufficientStateMappingException]
     * if there is mot enough information to do so.
     */
    @Suspendable
    override
    fun toTargetType(stateService: AccountsAwareStateService<MagazineContract.MagazineState>):
            MagazineContract.MagazineState {
        val publisherResolved = stateService.toAccountPartyOrNull(this.publisher, null, false,
                "publisher")
        val authorResolved = stateService.toAccountParty(this.author, null, false, "author")
        return MagazineContract.MagazineState(
                publisher = publisherResolved,
                author = authorResolved,
                price = this.price?:errNull("price"),
                genre = this.genre?:errNull("genre"),
                issues = this.issues?:errNull("issues"),
                title = this.title?:errNull("title"),
                published = this.published?:errNull("published"),
                linearId = this.linearId?:errNull("linearId")
        )
    }

    companion object {
        /**
         * Create a new DTO instance using the given [MagazineContract.MagazineState] as source.
         */
        @Suspendable
        fun from(original: MagazineContract.MagazineState,
                     stateService: AccountsAwareStateService<MagazineContract.MagazineState>):
                MagazineStateClientDto {
            val publisherResolved = stateService.toAccountInfoClientDtoOrNull(original.publisher)
            val authorResolved = stateService.toAccountInfoClientDto(original.author)
            return MagazineStateClientDto(
                    publisher = publisherResolved,
                    author = authorResolved,
                    price = original.price,
                    genre = original.genre,
                    issues = original.issues,
                    title = original.title,
                    published = original.published,
                    linearId = original.linearId
            )

        }
    }
}

Strategies

Both @VaultaireStateDto and @VaultaireStateDtoMixin support generation strategy hints. By default the strategy used is the REST-friendly VaultaireDtoStrategyKeys.CORDAPP_CLIENT_DTO. The only additional strategy provided is VaultaireDtoStrategyKeys.CORDAPP_LOCAL_DTO that supports no type conversions. Using both, as in the following example, will generate separate DTOs for each.

@VaultaireStateDto(
    ignoreProperties = ["foo"],
    strategies = [VaultaireDtoStrategyKeys.CORDAPP_CLIENT_DTO, VaultaireDtoStrategyKeys.CORDAPP_LOCAL_DTO]
)
data class BookState(
        //...
) : LinearState, QueryableState{
        //...
}