@@ -581,6 +581,44 @@ def test_idempotent_feature_view_projection_conversion():
581581 assert pydantic_obj == FeatureViewProjectionModel .model_validate (pydantic_json )
582582
583583
584+ def test_feature_view_projection_backwards_compatibility ():
585+ # Test deserialization with minimal fields (missing new optional fields)
586+ # https://github.com/ExpediaGroup/feast/pull/295/files#diff-5ad1ae3dd32afd2194e090c33d3661dcc68de8a49b1358f2a7c2796394e1f2fc
587+ minimal_dict = {
588+ "name" : "test_projection" ,
589+ "desired_features" : [],
590+ "features" : [],
591+ "join_key_map" : {},
592+ }
593+ pydantic_obj = FeatureViewProjectionModel .model_validate (minimal_dict )
594+ assert pydantic_obj .name == "test_projection"
595+ assert pydantic_obj .name_alias is None
596+ assert pydantic_obj .desired_features == []
597+ assert pydantic_obj .features == []
598+ assert pydantic_obj .join_key_map == {}
599+ assert pydantic_obj .timestamp_field is None
600+ assert pydantic_obj .date_partition_column is None
601+ assert pydantic_obj .created_timestamp_column is None
602+ assert pydantic_obj .batch_source is None
603+
604+ minimal_json = '{"name": "test_projection_json", "desired_features": [], "features": [], "join_key_map": {}}'
605+ pydantic_obj_from_json = FeatureViewProjectionModel .model_validate_json (
606+ minimal_json
607+ )
608+ assert pydantic_obj_from_json .name == "test_projection_json"
609+ assert pydantic_obj_from_json .timestamp_field is None
610+ assert pydantic_obj_from_json .date_partition_column is None
611+ assert pydantic_obj_from_json .created_timestamp_column is None
612+ assert pydantic_obj_from_json .batch_source is None
613+
614+ converted_python_obj = pydantic_obj .to_feature_view_projection ()
615+ assert converted_python_obj .name == "test_projection"
616+ assert converted_python_obj .timestamp_field is None
617+ assert converted_python_obj .date_partition_column is None
618+ assert converted_python_obj .created_timestamp_column is None
619+ assert converted_python_obj .batch_source is None
620+
621+
584622def test_idempotent_on_demand_feature_view_conversion ():
585623 tags = {
586624 "tag1" : "val1" ,
@@ -831,6 +869,67 @@ def test_idempotent_feature_service_conversion():
831869 assert pydantic_obj == FeatureServiceModel .model_validate (pydantic_json )
832870
833871
872+ def test_feature_service_backwards_compatibility ():
873+ # Test deserialization with feature_view_projections missing optional fields
874+ # https://github.com/ExpediaGroup/feast/pull/295/files#diff-5ad1ae3dd32afd2194e090c33d3661dcc68de8a49b1358f2a7c2796394e1f2fc
875+ field1 = Field (name = "feature1" , dtype = Float32 )
876+ field2 = Field (name = "feature2" , dtype = String )
877+ field1_model = FieldModel .from_field (field1 )
878+ field2_model = FieldModel .from_field (field2 )
879+
880+ minimal_dict = {
881+ "name" : "test_feature_service" ,
882+ "features" : [],
883+ "feature_view_projections" : [
884+ {
885+ "name" : "projection1" ,
886+ "desired_features" : ["feature1" , "feature2" ],
887+ "features" : [
888+ field1_model .model_dump (),
889+ field2_model .model_dump (),
890+ ],
891+ "join_key_map" : {"entity_id" : "entity_id" },
892+ },
893+ {
894+ "name" : "projection2" ,
895+ "desired_features" : [],
896+ "features" : [],
897+ "join_key_map" : {},
898+ },
899+ ],
900+ "description" : "Test feature service" ,
901+ "tags" : {"env" : "test" },
902+ "owner" : "test@example.com" ,
903+ "created_timestamp" : None ,
904+ "last_updated_timestamp" : None ,
905+ }
906+ pydantic_obj = FeatureServiceModel .model_validate (minimal_dict )
907+ assert pydantic_obj .name == "test_feature_service"
908+ assert len (pydantic_obj .feature_view_projections ) == 2
909+
910+ proj1 = pydantic_obj .feature_view_projections [0 ]
911+ assert proj1 .name == "projection1"
912+ assert proj1 .timestamp_field is None
913+ assert proj1 .date_partition_column is None
914+ assert proj1 .created_timestamp_column is None
915+ assert proj1 .batch_source is None
916+ assert len (proj1 .features ) == 2
917+
918+ proj2 = pydantic_obj .feature_view_projections [1 ]
919+ assert proj2 .name == "projection2"
920+ assert proj2 .timestamp_field is None
921+ assert proj2 .date_partition_column is None
922+ assert proj2 .created_timestamp_column is None
923+ assert proj2 .batch_source is None
924+
925+ pydantic_json = pydantic_obj .model_dump_json ()
926+ pydantic_obj_from_json = FeatureServiceModel .model_validate_json (pydantic_json )
927+ assert pydantic_obj_from_json .name == "test_feature_service"
928+ assert len (pydantic_obj_from_json .feature_view_projections ) == 2
929+ assert pydantic_obj_from_json .feature_view_projections [0 ].timestamp_field is None
930+ assert pydantic_obj_from_json .feature_view_projections [1 ].batch_source is None
931+
932+
834933def test_idempotent_project_metadata_conversion ():
835934 python_obj = ProjectMetadata (
836935 project_name = "test_project" ,
0 commit comments