diff --git a/lib/types/media-types.js b/lib/types/media-types.js
--- a/lib/types/media-types.js
+++ b/lib/types/media-types.js
@@ -1,13 +1,21 @@
 // @flow
 
+import t, { type TInterface, type TUnion } from 'tcomb';
+
 import type { Shape } from './core.js';
 import { type Platform } from './device-types.js';
+import { tShape, tString, tID } from '../utils/validation-utils.js';
 
 export type Dimensions = $ReadOnly<{
   +height: number,
   +width: number,
 }>;
 
+export const dimensionsValidator: TInterface<Dimensions> = tShape<Dimensions>({
+  height: t.Number,
+  width: t.Number,
+});
+
 export type MediaType = 'photo' | 'video';
 
 export type EncryptedMediaType = 'encrypted_photo' | 'encrypted_video';
@@ -246,6 +254,31 @@
       +duration: number, // seconds
     };
 
+export const mediaLibrarySelectionValidator: TUnion<MediaLibrarySelection> =
+  t.union([
+    tShape({
+      step: tString('photo_library'),
+      dimensions: dimensionsValidator,
+      filename: t.maybe(t.String),
+      uri: t.String,
+      mediaNativeID: t.maybe(t.String),
+      selectTime: t.Number,
+      sendTime: t.Number,
+      retries: t.Number,
+    }),
+    tShape({
+      step: tString('video_library'),
+      dimensions: dimensionsValidator,
+      filename: t.maybe(t.String),
+      uri: t.String,
+      mediaNativeID: t.maybe(t.String),
+      selectTime: t.Number,
+      sendTime: t.Number,
+      retries: t.Number,
+      duration: t.Number,
+    }),
+  ]);
+
 export type PhotoCapture = {
   +step: 'photo_capture',
   +time: number, // ms
@@ -258,6 +291,19 @@
   +retries: number,
 };
 
+export const photoCaptureValidator: TInterface<PhotoCapture> =
+  tShape<PhotoCapture>({
+    step: tString('photo_capture'),
+    time: t.Number,
+    dimensions: dimensionsValidator,
+    filename: t.String,
+    uri: t.String,
+    captureTime: t.Number,
+    selectTime: t.Number,
+    sendTime: t.Number,
+    retries: t.Number,
+  });
+
 export type PhotoPaste = {
   +step: 'photo_paste',
   +dimensions: Dimensions,
@@ -268,11 +314,28 @@
   +retries: number,
 };
 
+export const photoPasteValidator: TInterface<PhotoPaste> = tShape<PhotoPaste>({
+  step: tString('photo_paste'),
+  dimensions: dimensionsValidator,
+  filename: t.String,
+  uri: t.String,
+  selectTime: t.Number,
+  sendTime: t.Number,
+  retries: t.Number,
+});
+
 export type NativeMediaSelection =
   | MediaLibrarySelection
   | PhotoCapture
   | PhotoPaste;
 
+export const nativeMediaSelectionValidator: TUnion<NativeMediaSelection> =
+  t.union([
+    mediaLibrarySelectionValidator,
+    photoCaptureValidator,
+    photoPasteValidator,
+  ]);
+
 export type MediaMissionStep =
   | NativeMediaSelection
   | {
@@ -560,6 +623,14 @@
   +localMediaSelection?: NativeMediaSelection,
 };
 
+export const imageValidator: TInterface<Image> = tShape<Image>({
+  id: tID,
+  uri: t.String,
+  type: tString('photo'),
+  dimensions: dimensionsValidator,
+  localMediaSelection: t.maybe(nativeMediaSelectionValidator),
+});
+
 export type EncryptedImage = {
   +id: string,
   // a media URI for keyserver uploads / blob holder for Blob service uploads
@@ -569,6 +640,15 @@
   +dimensions: Dimensions,
 };
 
+export const encryptedImageValidator: TInterface<EncryptedImage> =
+  tShape<EncryptedImage>({
+    id: tID,
+    holder: t.String,
+    encryptionKey: t.String,
+    type: tString('encrypted_photo'),
+    dimensions: dimensionsValidator,
+  });
+
 export type Video = {
   +id: string,
   +uri: string,
@@ -581,6 +661,17 @@
   +localMediaSelection?: NativeMediaSelection,
 };
 
+export const videoValidator: TInterface<Video> = tShape<Video>({
+  id: tID,
+  uri: t.String,
+  type: tString('video'),
+  dimensions: dimensionsValidator,
+  loop: t.maybe(t.Boolean),
+  thumbnailID: tID,
+  thumbnailURI: t.String,
+  localMediaSelection: t.maybe(nativeMediaSelectionValidator),
+});
+
 export type EncryptedVideo = {
   +id: string,
   // a media URI for keyserver uploads / blob holder for Blob service uploads
@@ -594,4 +685,24 @@
   +thumbnailEncryptionKey: string,
 };
 
+export const encryptedVideoValidator: TInterface<EncryptedVideo> =
+  tShape<EncryptedVideo>({
+    id: tID,
+    holder: t.String,
+    encryptionKey: t.String,
+    type: tString('encrypted_video'),
+    dimensions: dimensionsValidator,
+    loop: t.maybe(t.Boolean),
+    thumbnailID: tID,
+    thumbnailHolder: t.String,
+    thumbnailEncryptionKey: t.String,
+  });
+
 export type Media = Image | Video | EncryptedImage | EncryptedVideo;
+
+export const mediaValidator: TUnion<Media> = t.union([
+  imageValidator,
+  videoValidator,
+  encryptedImageValidator,
+  encryptedVideoValidator,
+]);
diff --git a/lib/types/validation.test.js b/lib/types/validation.test.js
new file mode 100644
--- /dev/null
+++ b/lib/types/validation.test.js
@@ -0,0 +1,44 @@
+// @flow
+
+import {
+  imageValidator,
+  videoValidator,
+  mediaValidator,
+} from './media-types.js';
+
+describe('media validation', () => {
+  const photo = {
+    id: '92696',
+    type: 'photo',
+    uri: 'http://0.0.0.0:3000/comm/upload/92696/0fb272bd1c75d976',
+    dimensions: {
+      width: 340,
+      height: 288,
+    },
+  };
+  const video = {
+    type: 'video',
+    id: '92769',
+    uri: 'http://0.0.0.0:3000/comm/upload/92769/4bcc6987b25b2f66',
+    dimensions: {
+      width: 480,
+      height: 270,
+    },
+    thumbnailID: '92770',
+    thumbnailURI: 'http://0.0.0.0:3000/comm/upload/92770/d56466051dcef1db',
+  };
+
+  it('should validate correct media', () => {
+    expect(mediaValidator.is(photo)).toBe(true);
+    expect(imageValidator.is(photo)).toBe(true);
+    expect(mediaValidator.is(video)).toBe(true);
+    expect(videoValidator.is(video)).toBe(true);
+  });
+
+  it('should not validate incorrect media', () => {
+    expect(imageValidator.is(video)).toBe(false);
+    expect(videoValidator.is(photo)).toBe(false);
+    expect(mediaValidator.is({ ...photo, type: undefined })).toBe(false);
+    expect(mediaValidator.is({ ...video, dimensions: undefined })).toBe(false);
+  });
+});