diff --git a/keyserver/src/utils/validation-utils.js b/keyserver/src/utils/validation-utils.js
--- a/keyserver/src/utils/validation-utils.js
+++ b/keyserver/src/utils/validation-utils.js
@@ -1,5 +1,9 @@
 // @flow
 
+import _mapKeys from 'lodash/fp/mapKeys.js';
+import _mapValues from 'lodash/fp/mapValues.js';
+import type { TType, TInterface } from 'tcomb';
+
 import type { PolicyType } from 'lib/facts/policies.js';
 import { hasMinCodeVersion } from 'lib/shared/version-utils.js';
 import { ServerError } from 'lib/utils/errors.js';
@@ -8,20 +12,25 @@
   tPassword,
   tPlatform,
   tPlatformDetails,
+  assertWithValidator,
 } from 'lib/utils/validation-utils.js';
 
 import { fetchNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js';
 import { verifyClientSupported } from '../session/version.js';
 import type { Viewer } from '../session/viewer.js';
 
-async function validateInput(viewer: Viewer, inputValidator: *, input: *) {
+async function validateInput<T>(
+  viewer: Viewer,
+  inputValidator: ?TType<T>,
+  input: T,
+) {
   if (!viewer.isSocket) {
     await checkClientSupported(viewer, inputValidator, input);
   }
   checkInputValidator(inputValidator, input);
 }
 
-function checkInputValidator(inputValidator: *, input: *) {
+function checkInputValidator<T>(inputValidator: ?TType<T>, input: T) {
   if (!inputValidator || inputValidator.is(input)) {
     return;
   }
@@ -30,10 +39,10 @@
   throw error;
 }
 
-async function checkClientSupported(
+async function checkClientSupported<T>(
   viewer: Viewer,
-  inputValidator: *,
-  input: *,
+  inputValidator: ?TType<T>,
+  input: T,
 ) {
   let platformDetails;
   if (inputValidator) {
@@ -61,34 +70,13 @@
 
 const redactedString = '********';
 const redactedTypes = [tPassword, tCookie];
-function sanitizeInput(inputValidator: any, input: any): any {
-  if (!inputValidator) {
-    return input;
-  }
-  if (redactedTypes.includes(inputValidator) && typeof input === 'string') {
-    return redactedString;
-  }
-  if (
-    inputValidator.meta.kind === 'maybe' &&
-    redactedTypes.includes(inputValidator.meta.type) &&
-    typeof input === 'string'
-  ) {
-    return redactedString;
-  }
-  if (
-    inputValidator.meta.kind !== 'interface' ||
-    typeof input !== 'object' ||
-    !input
-  ) {
-    return input;
-  }
-  const result = {};
-  for (const key in input) {
-    const value = input[key];
-    const validator = inputValidator.meta.props[key];
-    result[key] = sanitizeInput(validator, value);
-  }
-  return result;
+function sanitizeInput<T>(inputValidator: TType<T>, input: T): T {
+  return convertObject(
+    inputValidator,
+    input,
+    redactedTypes,
+    () => redactedString,
+  );
 }
 
 function findFirstInputMatchingValidator(
@@ -156,6 +144,87 @@
   return null;
 }
 
+function convertObject<T, I>(
+  validator: TType<I>,
+  input: I,
+  typesToConvert: $ReadOnlyArray<TType<T>>,
+  conversionFunction: T => T,
+): I {
+  if (input === null || input === undefined) {
+    return input;
+  }
+
+  // While they should be the same runtime object,
+  // `TValidator` is `TType<T>` and `validator` is `TType<I>`.
+  // Having them have different types allows us to use `assertWithValidator`
+  // to change `input` flow type
+  const TValidator = typesToConvert[typesToConvert.indexOf(validator)];
+  if (TValidator && TValidator.is(input)) {
+    const TInput = assertWithValidator(input, TValidator);
+    const converted = conversionFunction(TInput);
+    return assertWithValidator(converted, validator);
+  }
+
+  if (validator.meta.kind === 'maybe') {
+    return convertObject(
+      validator.meta.type,
+      input,
+      typesToConvert,
+      conversionFunction,
+    );
+  }
+  if (validator.meta.kind === 'interface' && typeof input === 'object') {
+    const recastValidator: TInterface<typeof input> = (validator: any);
+    const result = {};
+    for (const key in input) {
+      const innerValidator = recastValidator.meta.props[key];
+      result[key] = convertObject(
+        innerValidator,
+        input[key],
+        typesToConvert,
+        conversionFunction,
+      );
+    }
+    return assertWithValidator(result, recastValidator);
+  }
+  if (validator.meta.kind === 'union') {
+    for (const innerValidator of validator.meta.types) {
+      if (innerValidator.is(input)) {
+        return convertObject(
+          innerValidator,
+          input,
+          typesToConvert,
+          conversionFunction,
+        );
+      }
+    }
+    return input;
+  }
+  if (validator.meta.kind === 'list' && Array.isArray(input)) {
+    const innerValidator = validator.meta.type;
+    return (input.map(value =>
+      convertObject(innerValidator, value, typesToConvert, conversionFunction),
+    ): any);
+  }
+  if (validator.meta.kind === 'dict' && typeof input === 'object') {
+    const domainValidator = validator.meta.domain;
+    const codomainValidator = validator.meta.codomain;
+    if (typesToConvert.includes(domainValidator)) {
+      input = _mapKeys(key => conversionFunction(key))(input);
+    }
+    return _mapValues(value =>
+      convertObject(
+        codomainValidator,
+        value,
+        typesToConvert,
+        conversionFunction,
+      ),
+    )(input);
+  }
+
+  return input;
+}
+
 async function policiesValidator(
   viewer: Viewer,
   policies: $ReadOnlyArray<PolicyType>,
@@ -186,5 +255,6 @@
   sanitizeInput,
   findFirstInputMatchingValidator,
   checkClientSupported,
+  convertObject,
   policiesValidator,
 };
diff --git a/keyserver/src/utils/validation-utils.test.js b/keyserver/src/utils/validation-utils.test.js
--- a/keyserver/src/utils/validation-utils.test.js
+++ b/keyserver/src/utils/validation-utils.test.js
@@ -24,4 +24,51 @@
     const redacted = { password: redactedString };
     expect(sanitizeInput(validator, object)).toStrictEqual(redacted);
   });
+
+  it('should redact a string in optional object', () => {
+    const validator = tShape({ obj: t.maybe(tShape({ password: tPassword })) });
+    const object = { obj: { password: 'password' } };
+    const redacted = { obj: { password: redactedString } };
+    expect(sanitizeInput(validator, object)).toStrictEqual(redacted);
+  });
+
+  it('should redact a string array', () => {
+    const validator = tShape({ passwords: t.list(tPassword) });
+    const object = { passwords: ['password', 'password'] };
+    const redacted = { passwords: [redactedString, redactedString] };
+    expect(sanitizeInput(validator, object)).toStrictEqual(redacted);
+  });
+
+  it('should redact a string inside a dict', () => {
+    const validator = tShape({ passwords: t.dict(t.String, tPassword) });
+    const object = { passwords: { a: 'password', b: 'password' } };
+    const redacted = { passwords: { a: redactedString, b: redactedString } };
+    expect(sanitizeInput(validator, object)).toStrictEqual(redacted);
+  });
+
+  it('should redact password dict key', () => {
+    const validator = tShape({ passwords: t.dict(tPassword, t.Bool) });
+    const object = { passwords: { password1: true, password2: false } };
+    const redacted = { passwords: {} };
+    redacted.passwords[redactedString] = false;
+    expect(sanitizeInput(validator, object)).toStrictEqual(redacted);
+  });
+
+  it('should redact a string inside a union', () => {
+    const validator = tShape({
+      password: t.union([tPassword, t.String, t.Bool]),
+    });
+    const object = { password: 'password' };
+    const redacted = { password: redactedString };
+    expect(sanitizeInput(validator, object)).toStrictEqual(redacted);
+  });
+
+  it('should redact a string inside an object array', () => {
+    const validator = tShape({
+      passwords: t.list(tShape({ password: tPassword })),
+    });
+    const object = { passwords: [{ password: 'password' }] };
+    const redacted = { passwords: [{ password: redactedString }] };
+    expect(sanitizeInput(validator, object)).toStrictEqual(redacted);
+  });
 });
diff --git a/lib/utils/validation-utils.js b/lib/utils/validation-utils.js
--- a/lib/utils/validation-utils.js
+++ b/lib/utils/validation-utils.js
@@ -1,5 +1,6 @@
 // @flow
 
+import invariant from 'invariant';
 import t from 'tcomb';
 import type {
   TStructProps,
@@ -8,6 +9,7 @@
   TEnums,
   TInterface,
   TUnion,
+  TType,
 } from 'tcomb';
 
 import {
@@ -94,6 +96,11 @@
   tMediaMessageVideo,
 ]);
 
+function assertWithValidator<T>(data: mixed, validator: TType<T>): T {
+  invariant(validator.is(data), "data isn't of type T");
+  return (data: any);
+}
+
 export {
   tBool,
   tString,
@@ -114,4 +121,5 @@
   tMediaMessagePhoto,
   tMediaMessageVideo,
   tMediaMessageMedia,
+  assertWithValidator,
 };