DCQL Authoring
A DCQL query asks a wallet for a set of credentials and the specific claims to disclose. Writing one by hand requires knowing the credential-type binding registry, the claim path conventions used by every credential format the tenant accepts, and the credential_sets semantics for combining required and alternative credentials. The EDK lets the author work at a higher level: select attributes from semantic attribute sets that the tenant has already published, set a small number of flags, and let SemanticToDcqlConverter produce the DCQL JSON mechanically.
The same converter sits behind two different authoring entry points. A lightweight "give me a query that asks for these three attributes" call (used by tooling, scripts, and the admin UI's quick-create) builds a request directly. The enterprise authoring path layers a richer UI on top with set browsing, attribute search, and preview, then still produces the same request and hands it to the same converter. The DCQL it emits is identical in both cases.
The Conversion Contract
interface SemanticToDcqlConverter {
suspend fun convert(
request: SemanticToDcqlConversionRequest,
): IdkResult<DcqlConversionResult, IdkError>
}
The request lists which attribute sets the author wants to include, optionally restricts the attributes within each set to a chosen subset, and sets two DCQL-level flags. The result carries the assembled DcqlQuery plus the provenance information that the authoring layer needs to snapshot per version (which sets contributed, which attribute paths were resolved, which claim paths the converter emitted, and any warnings).
data class AttributeSelection(
val attributeSetId: Uuid,
val selectedPaths: List<String> = emptyList(),
)
data class SemanticToDcqlConversionRequest(
val selections: List<AttributeSelection>,
val intentToRetain: Boolean = false,
val useCredentialSets: Boolean = false,
)
data class DcqlConversionResult(
val dcqlQuery: DcqlQuery,
val attributeSetIds: List<Uuid>,
val selectedAttributePaths: List<String>,
val generatedClaimPaths: Map<String, List<List<String>>>,
val warnings: List<String> = emptyList(),
)
An empty selectedPaths means "include every attribute in this set". A non-empty list filters to the attributes whose path matches one of the strings; unresolved paths produce a warning in the result rather than a hard failure, so the caller can show the author what was missing and let them decide whether to commit anyway.
How the Converter Works
The converter walks the request in four phases.
Resolve sets. Every attributeSetId is looked up in the SemanticAttributeSet registry. A missing set is a fatal error; missing attribute paths within a present set are non-fatal warnings.
Resolve candidate credentials. Each set has an associated CredentialTypeBinding registry entry listing the credential-type candidates that carry its attributes (for example, an "identity" attribute set might bind to both org.iso.18013.5.1.mDL and the SD-JWT identity_credential VCT). The converter pulls the candidate list per set.
Emit claim queries. For each candidate credential, the converter emits a DcqlClaimQuery per resolved attribute path. The claim path is translated from the attribute's semantic path to the credential format's actual claim path through the binding. intent_to_retain from the request is stamped on every emitted claim query when set.
Wrap as needed. When useCredentialSets is true, the converter wraps every candidate credential id in a single DcqlCredentialSetQuery with required = true and a single options list naming all candidates. This is the right shape when the author wants "any one of these credentials is acceptable", as opposed to the default behaviour where each candidate is an independent credential the wallet may or may not have.
The result preserves the input order of selections in attributeSetIds, and groups the emitted claim paths by credential-query id in generatedClaimPaths. The authoring layer uses these to display a structured preview of the DCQL the wallet will see before the author commits.
Wiring Authoring into the Versioned Store
The converter is stateless and does not write to the DCQL store. The authoring flow is:
- The author selects attribute sets and paths in the UI (or in code).
- The UI calls
SemanticToDcqlConverter.convert(...)and renders the result. - The author reviews
dcqlQuery,generatedClaimPaths, and anywarnings. - The UI calls the DCQL admin REST API to persist the result:
POST /api/v1/oid4vp/dcqlfor a new query,PUTorPATCH /api/v1/oid4vp/dcql/{queryId}to update an existing one. The body is aDcqlQueryConfigurationcarrying the converter'sdcqlQueryas the body. - The store appends a new immutable version and advances the current-version pointer.
The provenance fields on DcqlConversionResult (which attribute sets contributed, which paths were resolved) are typically persisted alongside the DCQL query by the authoring layer in a separate table, so that the author can later open the same query, see what it was generated from, and re-author it without rebuilding the selection from scratch. The version history in the DCQL store proper only holds the resulting DcqlQuery body; the authoring provenance lives in the authoring module's own store.
Example
A tenant publishes an "Identity basics" attribute set with paths given_name, family_name, birth_date. The bindings registry says this set is carried by two credential candidates: SD-JWT VCT https://issuer.example.com/identity and mDoc doctype org.iso.18013.5.1.mDL. The author wants a query asking for first and last name only, accepting either credential.
val result = converter.convert(
SemanticToDcqlConversionRequest(
selections = listOf(
AttributeSelection(
attributeSetId = identityBasicsSetId,
selectedPaths = listOf("given_name", "family_name"),
),
),
intentToRetain = false,
useCredentialSets = true,
),
)
The converter resolves the set, finds the two candidates, emits a DcqlCredentialQuery for each (with claim queries for given_name and family_name translated to the SD-JWT and mDoc claim paths respectively), and wraps both candidate ids in a DcqlCredentialSetQuery with required = true. The author commits the result through POST /api/v1/oid4vp/dcql and the versioned store writes version 1.
Errors and Warnings
Errors come back as IdkError and are fatal:
- Missing or deleted attribute set:
NOT_FOUND_ERROR. - No credential-type bindings registered for the set:
ILLEGAL_ARGUMENT_ERROR(the converter cannot emit aDcqlCredentialQuerywithout at least one candidate).
Warnings are returned in the successful result and are not fatal:
- An entry in
selectedPathsdid not match any attribute in the set, the warning names the path and the set. - A candidate credential's binding does not cover one of the selected attributes; the converter skips that attribute for that candidate (it may still be emitted for other candidates) and warns.
The authoring UI is expected to display warnings and let the author decide whether to commit. The DCQL store does not re-validate against the binding registry on write, so a query committed with warnings remains as-authored.