diff --git a/sui_core/Cargo.toml b/sui_core/Cargo.toml
index bc8f6e30ac3d6..ffe2f3c11eccf 100644
--- a/sui_core/Cargo.toml
+++ b/sui_core/Cargo.toml
@@ -28,6 +28,7 @@ sui-network = { path = "../network_utils" }
 sui-types = { path = "../sui_types" }
 
 move-binary-format = { git = "https://github.com/diem/move", rev="1e5e36cb1fede8073c8688886a0943bb5bfe40d5" }
+move-bytecode-utils = { git = "https://github.com/diem/move", rev="1e5e36cb1fede8073c8688886a0943bb5bfe40d5" }
 move-core-types = { git = "https://github.com/diem/move", rev="1e5e36cb1fede8073c8688886a0943bb5bfe40d5", features=["address20"] }
 move-package = { git = "https://github.com/diem/move", rev="1e5e36cb1fede8073c8688886a0943bb5bfe40d5" }
 move-vm-runtime = { git = "https://github.com/diem/move", rev="1e5e36cb1fede8073c8688886a0943bb5bfe40d5" }
diff --git a/sui_core/src/authority.rs b/sui_core/src/authority.rs
index cf48e14f631fc..b41bf41538520 100644
--- a/sui_core/src/authority.rs
+++ b/sui_core/src/authority.rs
@@ -1,6 +1,7 @@
 // Copyright (c) Facebook, Inc. and its affiliates.
 // SPDX-License-Identifier: Apache-2.0
 
+use move_bytecode_utils::module_cache::ModuleCache;
 use move_core_types::{
     account_address::AccountAddress,
     language_storage::{ModuleId, StructTag},
@@ -439,8 +440,19 @@ impl AuthorityState {
                 } else {
                     self.get_order_lock(&object.to_object_reference()).await?
                 };
+                let layout = match request.request_layout {
+                    Some(format) => {
+                        let resolver = ModuleCache::new(&self);
+                        object.get_layout(format, &resolver)?
+                    }
+                    None => None,
+                };
 
-                Some(ObjectResponse { object, lock })
+                Some(ObjectResponse {
+                    object,
+                    lock,
+                    layout,
+                })
             }
             Err(e) => return Err(e),
             _ => None,
@@ -630,3 +642,11 @@ impl AuthorityState {
         Ok(filtered)
     }
 }
+
+impl ModuleResolver for AuthorityState {
+    type Error = SuiError;
+
+    fn get_module(&self, module_id: &ModuleId) -> Result<Option<Vec<u8>>, Self::Error> {
+        self._database.get_module(module_id)
+    }
+}
diff --git a/sui_core/src/authority/authority_store.rs b/sui_core/src/authority/authority_store.rs
index 7f9fa8be8f58c..cc3451868aaf1 100644
--- a/sui_core/src/authority/authority_store.rs
+++ b/sui_core/src/authority/authority_store.rs
@@ -451,3 +451,22 @@ impl AuthorityStore {
         }))
     }
 }
+
+impl ModuleResolver for AuthorityStore {
+    type Error = SuiError;
+
+    fn get_module(&self, module_id: &ModuleId) -> Result<Option<Vec<u8>>, Self::Error> {
+        match self.get_object(module_id.address())? {
+            Some(o) => match &o.data {
+                Data::Package(c) => Ok(c
+                    .get(module_id.name().as_str())
+                    .cloned()
+                    .map(|m| m.into_vec())),
+                _ => Err(SuiError::BadObjectType {
+                    error: "Expected module object".to_string(),
+                }),
+            },
+            None => Ok(None),
+        }
+    }
+}
diff --git a/sui_core/src/authority_aggregator.rs b/sui_core/src/authority_aggregator.rs
index 9ea6b2c6ab407..ee22f84b4afb1 100644
--- a/sui_core/src/authority_aggregator.rs
+++ b/sui_core/src/authority_aggregator.rs
@@ -427,11 +427,17 @@ where
                 // None if the object was deleted at this authority
                 //
                 // NOTE: here we could also be gathering the locked orders to see if we could make a cert.
-                let (object_option, signed_order_option) =
-                    if let Some(ObjectResponse { object, lock }) = object_and_lock {
-                        (Some(object), lock)
+                // TODO: pass along layout_option so the client can store it
+                let (object_option, signed_order_option, _layout_option) =
+                    if let Some(ObjectResponse {
+                        object,
+                        lock,
+                        layout,
+                    }) = object_and_lock
+                    {
+                        (Some(object), lock, layout)
                     } else {
-                        (None, None)
+                        (None, None, None)
                     };
 
                 // Update the map with the information from this authority
@@ -1072,6 +1078,8 @@ where
         let request = ObjectInfoRequest {
             object_id,
             request_sequence_number: None,
+            // TODO: allow caller to specify layout
+            request_layout: None,
         };
 
         // For now assume all authorities. Assume they're all honest
diff --git a/sui_types/src/messages.rs b/sui_types/src/messages.rs
index 2ee0cb3b8397a..ac8f0e3745706 100644
--- a/sui_types/src/messages.rs
+++ b/sui_types/src/messages.rs
@@ -1,7 +1,7 @@
 // Copyright (c) Facebook, Inc. and its affiliates.
 // SPDX-License-Identifier: Apache-2.0
 
-use crate::object::{Object, OBJECT_START_VERSION};
+use crate::object::{Object, ObjectFormatOptions, OBJECT_START_VERSION};
 
 use super::{base_types::*, committee::Committee, error::*, event::Event};
 
@@ -10,7 +10,7 @@ use super::{base_types::*, committee::Committee, error::*, event::Event};
 mod messages_tests;
 
 use move_binary_format::{access::ModuleAccess, CompiledModule};
-use move_core_types::{identifier::Identifier, language_storage::TypeTag};
+use move_core_types::{identifier::Identifier, language_storage::TypeTag, value::MoveStructLayout};
 use serde::{Deserialize, Serialize};
 use std::{
     collections::{BTreeSet, HashSet},
@@ -124,6 +124,8 @@ pub struct ObjectInfoRequest {
     pub object_id: ObjectID,
     /// The version of the object for which the parent certificate is sought.
     pub request_sequence_number: Option<SequenceNumber>,
+    /// If true, the request will return the layout of the object in the given format
+    pub request_layout: Option<ObjectFormatOptions>,
 }
 
 impl From<ObjectRef> for ObjectInfoRequest {
@@ -131,6 +133,7 @@ impl From<ObjectRef> for ObjectInfoRequest {
         ObjectInfoRequest {
             object_id: object_ref.0,
             request_sequence_number: Some(object_ref.1),
+            request_layout: None,
         }
     }
 }
@@ -140,6 +143,7 @@ impl From<ObjectID> for ObjectInfoRequest {
         ObjectInfoRequest {
             object_id,
             request_sequence_number: None,
+            request_layout: None,
         }
     }
 }
@@ -157,6 +161,9 @@ pub struct ObjectResponse {
     /// Order the object is locked on in this authority.
     /// None if the object is not currently locked by this authority.
     pub lock: Option<SignedOrder>,
+    /// Schema of the Move value inside this object.
+    /// None if the object is a Move package, or the request did not ask for the layout
+    pub layout: Option<MoveStructLayout>,
 }
 
 /// This message provides information about the latest object and its lock
diff --git a/sui_types/src/object.rs b/sui_types/src/object.rs
index b5820fe119e19..3f06c68d4b69e 100644
--- a/sui_types/src/object.rs
+++ b/sui_types/src/object.rs
@@ -47,7 +47,8 @@ const ID_END_INDEX: usize = AccountAddress::LENGTH;
 /// Index marking the end of the object's version + the beginning of type-specific data
 const VERSION_END_INDEX: usize = ID_END_INDEX + 8;
 
-/// Different schemes for converting a Move object to JSON
+/// Different schemes for converting a Move value into a structured representation
+#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize, Hash)]
 pub struct ObjectFormatOptions {
     /// If true, include the type of each object as well as its fields; e.g.:
     /// `{ "fields": { "f": 20, "g": { "fields" { "h": true }, "type": "0x0::MyModule::MyNestedType" }, "type": "0x0::MyModule::MyType" }`
@@ -400,6 +401,20 @@ impl Object {
         }
     }
 
+    /// Get a `MoveStructLayout` for `self`.
+    /// The `resolver` value must contain the module that declares `self.type_` and the (transitive)
+    /// dependencies of `self.type_` in order for this to succeed. Failure will result in an `ObjectSerializationError`
+    pub fn get_layout(
+        &self,
+        format: ObjectFormatOptions,
+        resolver: &impl GetModule,
+    ) -> Result<Option<MoveStructLayout>, SuiError> {
+        match &self.data {
+            Data::Move(m) => Ok(Some(m.get_layout(format, resolver)?)),
+            Data::Package(_) => Ok(None),
+        }
+    }
+
     /// Convert `self` to the JSON representation dictated by `format`.
     /// If `self` is a Move value, the `resolver` value must contain the module that declares `self.type_` and the (transitive)
     /// dependencies of `self.type_` in order for this to succeed. Failure will result in an `ObjectSerializationError`
@@ -465,3 +480,11 @@ impl Display for Object {
         )
     }
 }
+
+impl Default for ObjectFormatOptions {
+    fn default() -> Self {
+        ObjectFormatOptions {
+            include_types: true,
+        }
+    }
+}
diff --git a/sui_types/src/unit_tests/serialize_tests.rs b/sui_types/src/unit_tests/serialize_tests.rs
index 420c3a855b440..b36ee349fbe3e 100644
--- a/sui_types/src/unit_tests/serialize_tests.rs
+++ b/sui_types/src/unit_tests/serialize_tests.rs
@@ -51,10 +51,12 @@ fn test_info_request() {
     let req1 = ObjectInfoRequest {
         object_id: dbg_object_id(0x20),
         request_sequence_number: None,
+        request_layout: None,
     };
     let req2 = ObjectInfoRequest {
         object_id: dbg_object_id(0x20),
         request_sequence_number: Some(SequenceNumber::from(129)),
+        request_layout: None,
     };
 
     let buf1 = serialize_object_info_request(&req1);
@@ -244,6 +246,7 @@ fn test_info_response() {
         object_and_lock: Some(ObjectResponse {
             object: object.clone(),
             lock: Some(vote),
+            layout: None,
         }),
         parent_certificate: None,
         requested_object_reference: Some(object.to_object_reference()),