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

  1. Recompile and republish the calling contract against the same version of the target application crate that is actually deployed, then use the resulting ApplicationId
  2. 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
  3. 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
  4. 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

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


AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22). Data as JSON: /api/errors/f395da681d5131da. Report an issue: GitHub.