diff --git a/src/commands/vault.rs b/src/commands/vault.rs
index 2f6565915fbc399c6bb3a774d495456e1f00b5ba..cb8494587e24a2ac7ff28fb29570cadbc210f459 100644
--- a/src/commands/vault.rs
+++ b/src/commands/vault.rs
@@ -9,8 +9,8 @@ use crate::*;
 use age::secrecy::Secret;
 use anyhow::anyhow;
 use sea_orm::ActiveValue::Set;
-use sea_orm::ModelTrait;
 use sea_orm::{ConnectionTrait, TransactionTrait};
+use sea_orm::{DatabaseConnection, ModelTrait};
 use sp_core::crypto::AddressUri;
 use std::cell::RefCell;
 use std::io::{Read, Write};
@@ -187,7 +187,7 @@ pub struct AddressOrVaultNameGroup {
 	vault_name: Option<String>,
 }
 
-/// vault inspect for JSON serialization
+/// vault inspect result data
 #[derive(Serialize)]
 struct VaultInspectData {
 	substrate_uri: String,
@@ -198,6 +198,29 @@ struct VaultInspectData {
 	g1v1_public_key: Option<String>,
 }
 
+// Manual human-readable implementation
+impl SerializeHuman for VaultInspectData {
+	fn serialize_human(&self) -> String {
+		let mut lines = vec![
+			format!("Substrate URI: '{}'", self.substrate_uri),
+			format!(
+				"Crypto scheme: {}",
+				<&'static str>::from(self.crypto_scheme)
+			),
+			match &self.secret_seed {
+				None => "Secret seed/mini-secret: N/A".to_string(),
+				Some(seed) => format!("Secret seed/mini-secret: '0x{}'", seed),
+			},
+			format!("Public key (hex): '0x{}'", self.public_key_hex),
+			format!("SS58 Address: '{}'", self.ss58_address),
+		];
+		if let Some(ref g1v1) = self.g1v1_public_key {
+			lines.push(format!("(potential G1v1 public key: '{}')", g1v1));
+		}
+		lines.join("\n")
+	}
+}
+
 pub struct VaultDataToImport {
 	secret_format: SecretFormat,
 	secret_suri: String,
@@ -641,64 +664,10 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
 			password,
 			no_password,
 		} => {
-			let is_interactive = password.is_none() && !no_password;
-
-			let account_tree_node_to_inspect =
-				retrieve_account_tree_node(db, address_or_vault_name).await?;
-
-			let base_account_tree_node =
-				vault_account::get_base_account_tree_node(&account_tree_node_to_inspect);
-
-			let is_base_account =
-				Rc::ptr_eq(&account_tree_node_to_inspect, &base_account_tree_node);
-
-			if is_interactive && !is_base_account {
-				let base_account = base_account_tree_node.borrow().account.clone();
-				println!("The linked <Base> account is {base_account}");
-			}
-
-			// Handle password from non-interactive mode or ask for it
-			let password = if no_password {
-				String::new()
-			} else if let Some(password) = password {
-				password
-			} else {
-				println!("Enter password to decrypt the <Base> account key");
-				inputs::prompt_password()?
-			};
-
-			let vault_inspect_data =
-				compute_vault_inspect_data(&account_tree_node_to_inspect, password)?;
-
-			match data.args.output_format {
-				OutputFormat::Human => {
-					println!("Substrate URI: '{}'", vault_inspect_data.substrate_uri);
-					println!(
-						"Crypto scheme: {}",
-						<&'static str>::from(vault_inspect_data.crypto_scheme)
-					);
-					match vault_inspect_data.secret_seed {
-						None => println!("Secret seed/mini-secret: N/A"),
-						Some(secret_seed) => {
-							println!("Secret seed/mini-secret: '0x{}'", secret_seed)
-						}
-					}
-					println!(
-						"Public key (hex): '0x{}'",
-						vault_inspect_data.public_key_hex
-					);
-					println!("SS58 Address: '{}'", vault_inspect_data.ss58_address);
-					if let Some(g1v1_public_key) = vault_inspect_data.g1v1_public_key {
-						println!("(potential G1v1 public key: '{}')", g1v1_public_key);
-					}
-				}
-				OutputFormat::Json => {
-					println!(
-						"{}",
-						serde_json::to_string(&vault_inspect_data).map_err(|e| anyhow!(e))?
-					);
-				}
-			}
+			print_command_output_async(data.args.output_format, || {
+				handle_inspect_command(db, address_or_vault_name, password, no_password)
+			})
+			.await?
 		}
 		Subcommand::Migrate => {
 			println!("Migrating existing key files to db");
@@ -789,6 +758,40 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
 	Ok(())
 }
 
+async fn handle_inspect_command(
+	db: &DatabaseConnection,
+	address_or_vault_name: AddressOrVaultNameGroup,
+	password: Option<String>,
+	no_password: bool,
+) -> Result<VaultInspectData, GcliError> {
+	let is_interactive = password.is_none() && !no_password;
+
+	let account_tree_node_to_inspect =
+		retrieve_account_tree_node(db, address_or_vault_name).await?;
+
+	let base_account_tree_node =
+		vault_account::get_base_account_tree_node(&account_tree_node_to_inspect);
+
+	let is_base_account = Rc::ptr_eq(&account_tree_node_to_inspect, &base_account_tree_node);
+
+	if is_interactive && !is_base_account {
+		let base_account = base_account_tree_node.borrow().account.clone();
+		println!("The linked <Base> account is {base_account}");
+	}
+
+	// Handle password from non-interactive mode or ask for it
+	let password = if no_password {
+		String::new()
+	} else if let Some(password) = password {
+		password
+	} else {
+		println!("Enter password to decrypt the <Base> account key");
+		inputs::prompt_password()?
+	};
+
+	compute_vault_inspect_data(&account_tree_node_to_inspect, password)
+}
+
 fn compute_vault_inspect_data(
 	account_tree_node_to_inspect: &Rc<RefCell<AccountTreeNode>>,
 	password: String,
diff --git a/src/main.rs b/src/main.rs
index 8e417d6002ad0fefa59da870cc48aa307b4f7513..c29674e71da56dde30da36f7371de2efb6e3e289 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -82,7 +82,7 @@ pub struct Args {
 // TODO derive the fromstr implementation
 /// secret format
 #[derive(Clone, Copy, Debug, Eq, PartialEq, Default)]
-enum OutputFormat {
+pub enum OutputFormat {
 	/// Human
 	#[default]
 	Human,
@@ -114,6 +114,147 @@ impl From<OutputFormat> for OsStr {
 	}
 }
 
+pub trait SerializeJson {
+	fn serialize_json(&self) -> Result<String, GcliError>;
+}
+
+// Blanket implementation for all Serialize types
+impl<T> SerializeJson for T
+where
+	T: Serialize,
+{
+	fn serialize_json(&self) -> Result<String, GcliError> {
+		serde_json::to_string(self).map_err(GcliError::JsonError)
+	}
+}
+
+/// Structured Error for JSON OutputFormat to be serialized in JSON
+///
+/// Purpose is to translate a GcliError into a JSON object
+#[derive(serde::Serialize)]
+pub struct JsonOutputError {
+	pub error_type: String,
+	pub error: String,
+}
+
+impl GcliError {
+	pub fn error_type(&self) -> &'static str {
+		match self {
+			GcliError::Subxt(_) => "Subxt",
+			GcliError::Duniter(_) => "Duniter",
+			GcliError::Indexer(_) => "Indexer",
+			GcliError::DatabaseError(_) => "DatabaseError",
+			GcliError::Logic(_) => "Logic",
+			GcliError::Input(_) => "Input",
+			GcliError::Anyhow(_) => "Anyhow",
+			GcliError::IoError(_) => "IoError",
+			GcliError::JsonError(_) => "JsonError",
+		}
+	}
+
+	pub fn error_message(&self) -> String {
+		match self {
+			GcliError::Subxt(e) => e.to_string(),
+			GcliError::DatabaseError(e) => e.to_string(),
+			GcliError::Anyhow(e) => e.to_string(),
+			GcliError::IoError(e) => e.to_string(),
+			GcliError::JsonError(e) => e.to_string(),
+			GcliError::Duniter(e)
+			| GcliError::Indexer(e)
+			| GcliError::Logic(e)
+			| GcliError::Input(e) => e.clone(),
+		}
+	}
+}
+
+impl From<GcliError> for JsonOutputError {
+	fn from(gcli_error: GcliError) -> JsonOutputError {
+		JsonOutputError {
+			error_type: gcli_error.error_type().to_string(),
+			error: gcli_error.error_message(),
+		}
+	}
+}
+
+pub trait SerializeHuman {
+	fn serialize_human(&self) -> String;
+}
+
+// Shared logic
+fn print_command_output_impl<T>(
+	format: OutputFormat,
+	result: Result<T, GcliError>,
+) -> Result<(), GcliError>
+where
+	T: SerializeJson + SerializeHuman,
+{
+	match format {
+		OutputFormat::Human => match result {
+			Ok(value) => {
+				println!("{}", value.serialize_human());
+				Ok(())
+			}
+			Err(e) => Err(e),
+		},
+		OutputFormat::Json => match result {
+			Ok(value) => {
+				println!("{}", value.serialize_json()?);
+				Ok(())
+			}
+			Err(e) => {
+				// create a JsonOutputError and serialize it to JSON (don't want to break the JSON format)
+				let json_error: JsonOutputError = e.into();
+				println!("{}", serde_json::to_string(&json_error)?);
+				Ok(())
+			}
+		},
+	}
+}
+
+/// Prints the output of a fallible computation in the specified format.
+///
+/// This function takes an `OutputFormat` and a closure that returns a `Result<T, GcliError>`,
+/// where `T` implements both `SerializeJson` and `SerializeHuman`.
+///
+/// On success, it prints the value in either human-readable or JSON format.
+///
+/// On error, it simply returns the GcliError for human output, or as a structured JSON error object for JSON output.
+///
+/// # Arguments
+/// * `format` - The desired output format (`Human` or `Json`).
+/// * `f` - A closure that performs the computation and returns a `Result<T, GcliError>`.
+///
+/// # Returns
+/// * `Ok(())` if the output was printed successfully.
+/// * `Err(GcliError)` if an error occurred and the output format is `Human`.
+///
+/// # Example
+/// ```
+/// print_output(OutputFormat::Json, || my_command_handler(some, params))?;
+/// ```
+pub fn print_command_output<T, F>(format: OutputFormat, f: F) -> Result<(), GcliError>
+where
+	T: SerializeJson + SerializeHuman,
+	F: FnOnce() -> Result<T, GcliError>,
+{
+	let result = f();
+	print_command_output_impl(format, result)
+}
+
+/// Check [`print_command_output`] for documentation
+pub async fn print_command_output_async<T, F, Fut>(
+	format: OutputFormat,
+	f: F,
+) -> Result<(), GcliError>
+where
+	T: SerializeJson + SerializeHuman,
+	F: FnOnce() -> Fut,
+	Fut: std::future::Future<Output = Result<T, GcliError>>,
+{
+	let result = f().await;
+	print_command_output_impl(format, result)
+}
+
 /// define subcommands
 #[derive(Clone, Debug, clap::Subcommand, Default)]
 pub enum Subcommand {
diff --git a/src/utils.rs b/src/utils.rs
index 68ec0118e858a2cbae818b132772aa81cc2bacea..f931ca4280575c971e347f48c3d1cd9c4feac065 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -116,7 +116,11 @@ pub enum GcliError {
 	/// error coming from io
 	#[allow(dead_code)]
 	IoError(std::io::Error),
+	/// error coming from serde_json
+	#[allow(dead_code)]
+	JsonError(serde_json::error::Error),
 }
+
 impl std::fmt::Display for GcliError {
 	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
 		match self {
@@ -150,6 +154,11 @@ impl From<std::io::Error> for GcliError {
 		GcliError::IoError(error)
 	}
 }
+impl From<serde_json::error::Error> for GcliError {
+	fn from(error: serde_json::error::Error) -> Self {
+		GcliError::JsonError(error)
+	}
+}
 impl From<sea_orm::DbErr> for GcliError {
 	fn from(error: sea_orm::DbErr) -> Self {
 		GcliError::DatabaseError(error)