{"record":{"id":"f395da681d5131da","repo":"linera-io/linera-protocol","slug":"failed-to-deserialize-service-response","errorCode":null,"errorMessage":"Failed to deserialize service response","messagePattern":"Failed to deserialize service response","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"linera-sdk/src/contract/runtime.rs","lineNumber":365,"sourceCode":"    ) {\n        contract_wit::unsubscribe_from_events(chain_id.into(), application_id.into(), &name.into())\n    }\n\n    /// Queries an application service as an oracle and returns the response.\n    ///\n    /// Should only be used with queries where it is very likely that all validators will compute\n    /// the same result, otherwise most block proposals will fail.\n    ///\n    /// Cannot be used in fast blocks: A block using this call should be proposed by a regular\n    /// owner, not a super owner.\n    pub fn query_service<A: ServiceAbi + Send>(\n        &mut self,\n        application_id: ApplicationId<A>,\n        query: A::Query,\n    ) -> A::QueryResponse {\n        let query = serde_json::to_vec(&query).expect(\"Failed to serialize service query\");\n        let response = contract_wit::query_service(application_id.forget_abi().into(), &query);\n        serde_json::from_slice(&response).expect(\"Failed to deserialize service response\")\n    }\n\n    /// Opens a new chain, configuring it with the provided `chain_ownership` and\n    /// `application_permissions`, and crediting `balance` (debited from the current chain) to\n    /// `account` on the new chain. Use [`AccountOwner::CHAIN`] to fund the new chain's own\n    /// account, which is the only balance that pays fees for blocks it does not authenticate.\n    pub fn open_chain(\n        &mut self,\n        chain_ownership: ChainOwnership,\n        application_permissions: ApplicationPermissions,\n        account: AccountOwner,\n        balance: Amount,\n    ) -> ChainId {\n        let chain_id = contract_wit::open_chain(\n            &chain_ownership.into(),\n            &application_permissions.into(),\n            account.into(),\n            balance.into(),","sourceCodeStart":347,"sourceCodeEnd":383,"githubUrl":"https://github.com/linera-io/linera-protocol/blob/6c226ddcb332ef55118dc8d0aafbd093d5420899/linera-sdk/src/contract/runtime.rs#L347-L383","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: strict struct breaks when service adds/omits a key\nstruct QueryResponse { balance: Amount }\n\n// after: tolerant struct survives additive service changes\n#[derive(Deserialize)]\nstruct QueryResponse {\n    balance: Amount,\n    #[serde(default)]\n    extra: Option<serde_json::Value>,\n}","handlingStrategy":"validation","validationCode":"// Smoke-test the peer's schema before relying on it.\n#[test]\nfn query_response_schema() {\n    let sample = service_sample_response_json(); // from the target crate's fixtures\n    let _: A::QueryResponse = serde_json::from_str(&sample).expect(\"schema drift with target service\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["linera","rust","sdk","json","serde","service-query"],"backgroundTag":"json-deserialization-error","analyzedSha":"6c226ddcb332ef55118dc8d0aafbd093d5420899","analyzedAt":"2026-08-22T22:49:09.787Z","schemaVersion":2},"datasetVersion":"2026-08-23T01:17:44.959Z"}