A SavedModel is the handoff point where a trained TensorFlow model stops being only Python code and becomes a callable service artifact. Serving signatures define the request and response contract inside that artifact, so clients can send features and read scores without depending on auto-generated tensor names.
For current Keras models, model.export() is enough when the default serve endpoint is acceptable. tf.keras.export.ExportArchive is the better fit when the SavedModel needs explicit endpoint names, a named TensorSpec, and returned dictionary keys that line up with the payload serving clients send.
The signature name and field names become part of the deployment contract. Keep serving_default available for the primary inference path, inspect the exported SignatureDef with saved_model_cli, and avoid renaming inputs or outputs after another client has integrated with them.
Use model.export() when one default serve endpoint is enough, and switch to tf.keras.export.ExportArchive when serving needs named inputs, named outputs, or more than one endpoint.
export_signatures.py
.
import os import pathlib import shutil os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" import tensorflow as tf tf.get_logger().setLevel("ERROR") export_dir = pathlib.Path("priority_serving") if export_dir.exists(): shutil.rmtree(export_dir) model = tf.keras.Sequential( [ tf.keras.layers.Input(shape=(4,), name="features"), tf.keras.layers.Dense( 1, activation="sigmoid", name="priority_score", ), ], name="support_priority", ) model(tf.zeros((1, 4), dtype=tf.float32)) model.get_layer("priority_score").set_weights( [ tf.constant( [[1.2], [0.8], [-0.6], [1.0]], dtype=tf.float32, ).numpy(), tf.constant([-0.4], dtype=tf.float32).numpy(), ] ) @tf.function( input_signature=[ tf.TensorSpec( shape=(None, 4), dtype=tf.float32, name="features", ) ] ) def serve(features): scores = model(features, training=False) return {"scores": scores} @tf.function( input_signature=[ tf.TensorSpec( shape=(None, 4), dtype=tf.float32, name="features", ) ] ) def serve_labels(features): scores = model(features, training=False) labels = tf.cast(scores >= 0.5, tf.int32) return {"scores": scores, "labels": labels} export_archive = tf.keras.export.ExportArchive() export_archive.track(model) export_archive.add_endpoint(name="serve", fn=serve) export_archive.add_endpoint( name="labels", fn=serve_labels, ) export_archive.write_out(export_dir, verbose=False) artifact = tf.saved_model.load(str(export_dir)) sample_batch = tf.constant( [ [0.2, 0.9, 0.1, 0.8], [0.8, 0.3, 0.9, 0.2], ], dtype=tf.float32, ) serve_signature = artifact.signatures["serve"] labels_signature = artifact.signatures["labels"] serve_outputs = serve_signature(features=sample_batch) labels_outputs = labels_signature(features=sample_batch) signature_names = sorted(artifact.signatures.keys()) print("Primary signature: serve") print("Additional signature: labels") print(f"Default alias present: {'serving_default' in signature_names}") print(f"Serve keys: {sorted(serve_outputs.keys())}") print(f"Labels keys: {sorted(labels_outputs.keys())}") print(f"SavedModel dir: {export_dir}")
The TensorSpec name="features" value becomes the serving input key, while the returned dictionaries control the response field names.
Related: How to train, evaluate, and run prediction with a Keras model
$ python3 export_signatures.py Primary signature: serve Additional signature: labels Default alias present: True Serve keys: ['scores'] Labels keys: ['labels', 'scores'] SavedModel dir: priority_serving
The first endpoint added through add_endpoint() is also exposed as serving_default unless that exact name is registered manually, which is why both serve and serving_default appear here.
$ saved_model_cli show \
--dir priority_serving \
--tag_set serve \
--signature_def serving_default
The given SavedModel SignatureDef contains the following input(s):
inputs['features'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 4)
name: serving_default_features:0
The given SavedModel SignatureDef contains the following output(s):
outputs['scores'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 1)
name: StatefulPartitionedCall_2:0
Method name is: tensorflow/serving/predict
This is the request and response contract used when a serving client does not name a specific signature.
$ saved_model_cli show \
--dir priority_serving \
--tag_set serve \
--signature_def labels
The given SavedModel SignatureDef contains the following input(s):
inputs['features'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 4)
name: labels_features:0
The given SavedModel SignatureDef contains the following output(s):
outputs['labels'] tensor_info:
dtype: DT_INT32
shape: (-1, 1)
name: StatefulPartitionedCall:0
outputs['scores'] tensor_info:
dtype: DT_FLOAT
shape: (-1, 1)
name: StatefulPartitionedCall:1
Method name is: tensorflow/serving/predict
Custom signatures stay in the same SavedModel, which makes it possible to keep a standard serving_default endpoint for production traffic and expose a richer endpoint for operator-side checks or alternate consumers.
Changing the signature name, input key, or returned dictionary keys after a client has integrated with them breaks the request contract even when the model weights stay the same.
$ rm -r priority_serving export_signatures.py