linera-io/linera-protocol · error
Failed to deserialize service response
Error message
Failed to deserialize service response
What it means
When a contract queries a service via ContractRuntime::query_service, the query is JSON-serialized, sent through the host, and the response bytes are parsed with serde_json::from_slice::<A::QueryResponse>. The expect panics when the service's JSON response does not match the QueryResponse type the calling contract compiled against.
Source
Thrown at linera-sdk/src/contract/runtime.rs:365
) {
contract_wit::unsubscribe_from_events(chain_id.into(), application_id.into(), &name.into())
}
/// Queries an application service as an oracle and returns the response.
///
/// Should only be used with queries where it is very likely that all validators will compute
/// the same result, otherwise most block proposals will fail.
///
/// Cannot be used in fast blocks: A block using this call should be proposed by a regular
/// owner, not a super owner.
pub fn query_service<A: ServiceAbi + Send>(
&mut self,
application_id: ApplicationId<A>,
query: A::Query,
) -> A::QueryResponse {
let query = serde_json::to_vec(&query).expect("Failed to serialize service query");
let response = contract_wit::query_service(application_id.forget_abi().into(), &query);
serde_json::from_slice(&response).expect("Failed to deserialize service response")
}
/// Opens a new chain, configuring it with the provided `chain_ownership` and
/// `application_permissions`, and crediting `balance` (debited from the current chain) to
/// `account` on the new chain. Use [`AccountOwner::CHAIN`] to fund the new chain's own
/// account, which is the only balance that pays fees for blocks it does not authenticate.
pub fn open_chain(
&mut self,
chain_ownership: ChainOwnership,
application_permissions: ApplicationPermissions,
account: AccountOwner,
balance: Amount,
) -> ChainId {
let chain_id = contract_wit::open_chain(
&chain_ownership.into(),
&application_permissions.into(),
account.into(),
balance.into(),View on GitHub (pinned to 6c226ddcb3)
Solutions
- Recompile and republish the calling contract against the same version of the target application crate that is actually deployed, then use the resulting ApplicationId
- Verify the target service's query handler output against the caller's QueryResponse struct (field names, casing, Option vs required) with a serde round-trip test
- Make QueryResponse fields tolerant where possible (Option<T>, serde(default), integer types wide enough for serialized numbers) so minor service changes do not break decoding
- Confirm the ApplicationId was taken from the same genesis/network as the queried service
Example fix
// before: strict struct breaks when service adds/omits a key
struct QueryResponse { balance: Amount }
// after: tolerant struct survives additive service changes
#[derive(Deserialize)]
struct QueryResponse {
balance: Amount,
#[serde(default)]
extra: Option<serde_json::Value>,
} Defensive patterns
Strategy: validation
Validate before calling
// Smoke-test the peer's schema before relying on it.
#[test]
fn query_response_schema() {
let sample = service_sample_response_json(); // from the target crate's fixtures
let _: A::QueryResponse = serde_json::from_str(&sample).expect("schema drift with target service");
} Prevention
- Depend on the target application crate by exact version (e.g. '=0.4.x') in Cargo.toml so ABI drift fails at build time
- Prefer Option<T>/serde(default) fields in cross-app QueryResponse structs
- Resolve peer ApplicationIds through a registry updated on publish, not config files
- Log the raw response bytes in debug builds to make schema mismatches diagnosable
When it happens
Trigger: Calling runtime.query_service::<OtherApp>(application_id, query) where the target service returns JSON whose fields/types differ from the caller's OtherApp::QueryResponse (missing fields, renamed keys, different numeric types), or where application_id resolves to a different application than the ABI expects.
Common situations: Service module upgraded (response schema changed) while the calling contract still references the old ApplicationId; a QueryResponse field switched between Option and required; a response field switched from number to string; querying an application on a different Linera network whose modules differ.
Related errors
- Application parameters must be deserializable
- Failed to deserialize query response from application
- Failed to deserialize instantiation argument {argument:?}
- Query {argument:?} is invalid and could not be deserialized
- Application parameters must be deserializable
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/f395da681d5131da.
Report an issue: GitHub.